--- /dev/null
+share
+tags
+.coverage
+cmd/version_generated.go
+cmd/VERSION
+*~
+*.swp
+vendor/*/
+.spread-reuse*.yaml
+po/snappy.pot
+
+# snap-confine bits
+cmd/snap-confine/snap-confine
+cmd/snap-confine/snap-discard-ns
+cmd/snap-confine/snap-confine-unit-tests
+cmd/snap-confine/snap-confine.apparmor
+cmd/snap-confine/decode-mount-opts
+*~
+.*.swp
+*.o
+*.a
+
+# manual pages
+cmd/snap-confine/manpages/*.[1-9]
+
+# test-driver
+*.log
+*.trs
+
+# Automake
+Makefile
+Makefile.in
+snap-confine-*.tar.gz
+.deps
+
+# Autoconf
+aclocal.m4
+autom4te.cache
+compile
+config.guess
+config.h
+config.h.in
+config.status
+config.sub
+configure
+depcomp
+install-sh
+missing
+stamp-h1
+test-driver
--- /dev/null
+#!/bin/sh
+set -e
+echo "$@"
+# put me in the project root, call me ".precommit".
+# And put this here-document in ~/.bazaar/plugins/precommit_script.py:
+<<EOF
+import os
+
+if not os.getenv("SKIP_COMMIT_HOOK"):
+ import subprocess
+ from bzrlib.mutabletree import MutableTree
+ from bzrlib import errors
+
+ def start_commit_hook(*_):
+ """This hook will execute '.precommit' script from root path of the bazaar
+ branch. Commit will be canceled if precommit fails."""
+
+ # this hook only makes sense if a precommit file exist.
+ if not os.path.exists(".precommit"):
+ return
+ try:
+ subprocess.check_call(os.path.abspath(".precommit"))
+ # if precommit fails (process return not zero) cancel commit.
+ except subprocess.CalledProcessError:
+ raise errors.BzrError("pre commit check failed (set SKIP_COMMIT_HOOK to skip).")
+
+ MutableTree.hooks.install_named_hook('start_commit', start_commit_hook,
+ 'Run "precommit" script on start_commit')
+EOF
+
+./run-checks --quick
+
--- /dev/null
+#!/bin/sh
+
+set -ev
+
+# we always run in a fresh dir in tarmac
+export GOPATH=$(mktemp -d)
+trap 'rm -rf "$GOPATH"' EXIT
+
+# this is a hack, but not sure tarmac is golang friendly
+mkdir -p $GOPATH/src/github.com/snapcore/snapd
+cp -a . $GOPATH/src/github.com/snapcore/snapd/
+cd $GOPATH/src/github.com/snapcore/snapd
+
+sh -v ./run-checks
--- /dev/null
+sudo: required
+dist: trusty
+language: go
+go:
+ - 1.6
+
+env:
+ global:
+ # SPREAD_LINODE_KEY
+ - secure: "bzALrfNSLwM0bjceal1PU5rFErvqVhi00Sygx8jruo6htpZay3hrC2sHCKCQKPn1kvCfHidrHX1vnomg5N+B9o25GZEYSjKSGxuvdNDfCZYqPNjMbz5y7xXYfKWgyo+xtrKRM85Nqy121SfRz3KLDvrOLwwreb+pZv8DG1WraFTd7D6rK7nLnnYNUyw665XBMFVnM8ue3Zu9496Ih/TfQXhnNpsZY8xFWte4+cH7JvVCVTs8snjoGVZi3972PzinNkfBgJa24cUzxFMfiN/AwSBXJQKdVv+FsbB4uRgXAqTNwuus7PptiPNxpWWojuhm1Qgbk0XhGIdJxyUYkmNA4UrZ3C29nIRWbuAiHJ6ZWd1ur3dqphqOcgFInltSHkpfEdlL3YK4dCa2SmJESzotUGnyowCUUCXkWdDaZmFTwyK0Y6He9oyXDK5f+/U7SFlPvok0caJCvB9HbTQR1kYdh048I/R+Ht5QrFOZPk21DYWDOYhn7SzthBDZLsaL6n5gX7Y547SsL4B35YVbpaeHzccG6Mox8rI4bqlGFvP1U5i8uXD4uQjJChlVxpmozUEMok9T5RVediJs540p5uc8DQl48Nke02tXzC/XpGAvpnXT7eiiRNW67zOj2QcIV+ni3lBj3HvZeB9cgjzLNrZSl/t9vseqnNwQWpl3V6nd/bU="
+
+ matrix:
+ - RUN=static
+ - RUN=unit
+ - RUN=spread
+
+install:
+ - sudo apt-get update -qq
+ - sudo apt-get install -qq squashfs-tools
+ - sudo apt-get install -qq gnupg1 || sudo apt-get install -qq gnupg
+
+script: ./run-checks --$RUN
--- /dev/null
+Before contributing you should sign [Canonical's contributor agreement][1],
+it’s the easiest way for you to give us permission to use your contributions.
+
+## Pull Requests and tests
+
+We need to verify that the code functionality and quality is not degraded
+by additions before merging any changes to snapd's codebase. For each PR
+we run checks in three different groups: static, unit and spread.
+
+Static test use several code analysis tools present in the GoLang ecosystem
+(go vet, go lint and go fmt) to make sure that the code always aligns with
+the standards. They also check the markdown format of documentation files.
+All the existing unit tests are also executed, and the coverage info is
+reported to coveralls. Regarding [spread](https://github.com/snapcore/spread),
+we use it to verify the integrity of the product exercising it as a whole,
+both from an end user standpoint (eg. all kind of interactions with the
+snap tool from the command line) and from a more systemic approach (testing
+upgrades, for instance).
+
+We do not set as a requirement the addition of spread and unit tests for a PR
+to be merged, but encourage the contributors to add them so that the expected
+behaviour is explained and verified through the tests and the review process
+can be made on the solid base of a working system after the addition of the
+changes. If any tests need to be added for a PR to be merged it will be denoted
+during the review process.
+
+[1]: http://www.ubuntu.com/legal/contributors
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+# Hacking on snapd
+
+Hacking on snapd is fun and straightfoward. The code is extensively
+unit tested and we use the [spread](https://github.com/snapcore/spread)
+integration test framework for the integration/system level tests.
+
+## Development
+
+### Setting up a GOPATH
+
+When working with the source of Go programs, you should define a path within
+your home directory (or other workspace) which will be your `GOPATH`. `GOPATH`
+is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is documented
+[online](http://golang.org/pkg/go/build/) and inside the go tool itself
+
+ go help gopath
+
+Various conventions exist for naming the location of your `GOPATH`, but it
+should exist, and be writable by you. For example
+
+ export GOPATH=${HOME}/work
+ mkdir $GOPATH
+
+will define and create `$HOME/work` as your local `GOPATH`. The `go` tool
+itself will create three subdirectories inside your `GOPATH` when required;
+`src`, `pkg` and `bin`, which hold the source of Go programs, compiled packages
+and compiled binaries, respectively.
+
+Setting `GOPATH` correctly is critical when developing Go programs. Set and
+export it as part of your login script.
+
+Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install:
+
+ PATH="$PATH:$GOPATH/bin"
+
+### Getting the snapd sources
+
+The easiest way to get the source for `snapd` is to use the `go get` command.
+
+ go get -d -v github.com/snapcore/snapd/...
+
+This command will checkout the source of `snapd` and inspect it for any unmet
+Go package dependencies, downloading those as well. `go get` will also build
+and install `snapd` and its dependencies. To also build and install `snapd`
+itself into `$GOPATH/bin`, omit the `-d` flag. More details on the `go get`
+flags are available using
+
+ go help get
+
+At this point you will have the git local repository of the `snapd` source at
+`$GOPATH/src/github.com/snapcore/snapd`. The source for any
+dependent packages will also be available inside `$GOPATH`.
+
+### Dependencies handling
+
+Dependencies are handled via `govendor`. Get it via:
+
+ go get -u github.com/kardianos/govendor
+
+After a fresh checkout, move to the snapd source directory:
+
+ cd $GOPATH/src/github.com/snapcore/snapd
+
+And then, run:
+
+ govendor sync
+
+You can use the script `get-deps.sh` to run the two previous steps.
+
+If a dependency need updating
+
+ govendor fetch github.com/path/of/dependency
+
+### Building
+
+To build, once the sources are available and `GOPATH` is set, you can just run
+
+ go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap
+
+to get the `snap` binary in /tmp (or without -o to get it in the current
+working directory). Alternatively:
+
+ go install github.com/snapcore/snapd/...
+
+to have it available in `$GOPATH/bin`
+
+### Contributing
+
+Contributions are always welcome! Please make sure that you sign the
+Canonical contributor licence agreement at
+http://www.ubuntu.com/legal/contributors
+
+Snapd can be found on Github, so in order to fork the source and contribute,
+go to https://github.com/snapcore/snapd. Check out [Github's help
+pages](https://help.github.com/) to find out how to set up your local branch,
+commit changes and create pull requests.
+
+We value good tests, so when you fix a bug or add a new feature we highly
+encourage you to create a test in `$source_test.go`. See also the section
+about Testing.
+
+### Testing
+
+To run the various tests that we have to ensure a high quality source just run:
+
+ ./run-checks
+
+This will check if the source format is consistent, that it builds, all tests
+work as expected and that "go vet" has nothing to complain.
+
+You can run individual test for a sub-package by changing into that directory and:
+
+ go test -check.f $testname
+
+If a test hangs, you can enable verbose mode:
+
+ go test -v -check.vv
+
+(or -check.v for less verbose output).
+
+There is more to read about the testing framework on the [website](https://labix.org/gocheck)
+
+### Running the spread tests
+
+To run the spread tests locally you need the latest version of spread
+from https://github.com/snapcore/spread. It can be installed via:
+
+ $ sudo apt install qemu-kvm autopkgtest
+ $ sudo snap install --devmode spread
+
+Then setup the environment via:
+
+ $ mkdir -p .spread/qemu
+ $ cd .spread/qemu
+ # For xenial (same works for yakkety/zesty)
+ $ adt-buildvm-ubuntu-cloud -r xenial
+ $ mv adt-xenial-amd64-cloud.img ubuntu-16.04.img
+ # For trusty
+ $ adt-buildvm-ubuntu-cloud -r trusty --post-command='sudo apt-get install -y --install-recommends linux-generic-lts-xenial && update-grub'
+ $ mv adt-trusty-amd64-cloud.img ubuntu-14.04-64.img
+
+
+And you can run the tests via:
+
+ $ spread -v qemu:
+
+For quick reuse you can use:
+
+ $ spread -reuse qemu:
+
+It will print how to reuse the systems. Make sure to use
+`export REUSE_PROJECT=1` in your environment too.
+
+
+### Testing snapd
+
+To test the `snapd` REST API daemon on a snappy system you need to
+transfer it to the snappy system and then run:
+
+ sudo systemctl stop snapd.service snapd.socket
+ sudo /lib/systemd/systemd-activate -E SNAPD_DEBUG=3 -E SNAPD_DEBUG_HTTP=3 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd
+
+or with systemd version >= 230
+
+ sudo systemctl stop snapd.service snapd.socket
+ sudo systemd-socket-activate -E SNAPD_DEBUG=3 -E SNAPD_DEBUG_HTTP=3 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd
+
+This will stop the installed snapd and activate the new one. Once it's
+printed out something like `Listening on /run/snapd.socket as 3.` you
+should then
+
+ sudo chmod 0666 /run/snapd*.socket
+
+so the socket has the right permissions (otherwise you need `sudo` to
+connect).
+
+To debug interaction with the snap store, you can set `SNAP_DEBUG_HTTP`.
+It is a bitfield: dump requests: 1, dump responses: 2, dump bodies: 4.
+
+# Quick intro to hacking on snap-confine
+
+Hey, welcome to the nice, low-level world of snap-confine
+
+## Building the code locally
+
+To get started from a pristine tree you want to do this:
+
+```
+./mkversion.sh
+cd cmd/
+autoreconf -i -f
+./configure --prefix=/usr --libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu
+```
+
+This will drop makefiles and let you build stuff. You may find the `make hack`
+target, available in `cmd/snap-confine` handy, it installs the locally built
+version on your system and reloads the apparmor profile.
+
+## Submitting patches
+
+Please run `make fmt` before sending your patches.
--- /dev/null
+[![Build Status][travis-image]][travis-url]
+[![Go Report Card][goreportcard-image]][goreportcard-url]
+
+# snapd
+
+The snapd and snap tools enable systems to work with .snap files. See
+[snapcraft.io](http://snapcraft.io) for a high level overview about
+snap files and the snapd application.
+
+## Development
+
+To get started with development off the snapd code itself, please check
+out [HACKING.md](https://github.com/snapcore/snapd/blob/master/HACKING.md)
+for in-depth details.
+
+## Reporting bugs
+
+If you have found an issue with the application, please [file a bug](https://bugs.launchpad.net/snappy/+filebug) on the [bugs list on Launchpad](https://bugs.launchpad.net/snappy/).
+
+## Get in touch
+
+We're friendly! Talk to us on [IRC](https://webchat.freenode.net/?channels=snappy)
+or on [our mailing list](https://lists.snapcraft.io/mailman/listinfo/snapcraft).
+
+Get news and stay up to date on [Twitter](https://twitter.com/snapcraftio),
+[Google+](https://plus.google.com/+SnapcraftIo) or
+[Facebook](https://www.facebook.com/snapcraftio).
+
+
+
+[travis-image]: https://travis-ci.org/snapcore/snapd.svg?branch=master
+[travis-url]: https://travis-ci.org/snapcore/snapd
+
+[goreportcard-image]: https://goreportcard.com/badge/github.com/snapcore/snapd
+[goreportcard-url]: https://goreportcard.com/report/github.com/snapcore/snapd
+
+[coveralls-image]: https://coveralls.io/repos/snapcore/snapd/badge.svg?branch=master&service=github
+[coveralls-url]: https://coveralls.io/github/snapcore/snapd?branch=master
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package arch
+
+import (
+ "log"
+ "runtime"
+)
+
+// ArchitectureType is the type for a supported snappy architecture
+type ArchitectureType string
+
+// arch is global to allow tools like ubuntu-device-flash to
+// change the architecture. This is important to e.g. install
+// armhf snaps onto a armhf image that is generated on an amd64
+// machine
+var arch = ArchitectureType(ubuntuArchFromGoArch(runtime.GOARCH))
+
+// SetArchitecture allows overriding the auto detected Architecture
+func SetArchitecture(newArch ArchitectureType) {
+ arch = newArch
+}
+
+// UbuntuArchitecture returns the debian equivalent architecture for the
+// currently running architecture.
+//
+// If the architecture does not map any debian architecture, the
+// GOARCH is returned.
+func UbuntuArchitecture() string {
+ return string(arch)
+}
+
+// ubuntuArchFromGoArch maps a go architecture string to the coresponding
+// Ubuntu architecture string.
+//
+// E.g. the go "386" architecture string maps to the ubuntu "i386"
+// architecture.
+func ubuntuArchFromGoArch(goarch string) string {
+ goArchMapping := map[string]string{
+ // go ubuntu
+ "386": "i386",
+ "amd64": "amd64",
+ "arm": "armhf",
+ "arm64": "arm64",
+ "ppc64le": "ppc64el",
+ "s390x": "s390x",
+ "ppc": "powerpc",
+ }
+
+ ubuntuArch := goArchMapping[goarch]
+ if ubuntuArch == "" {
+ log.Panicf("unknown goarch %v", goarch)
+ }
+
+ return ubuntuArch
+}
+
+// IsSupportedArchitecture returns true if the system architecture is in the
+// list of architectures.
+func IsSupportedArchitecture(architectures []string) bool {
+ for _, a := range architectures {
+ if a == "all" || a == string(arch) {
+ return true
+ }
+ }
+
+ return false
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package arch
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&ArchTestSuite{})
+
+type ArchTestSuite struct {
+}
+
+func (ts *ArchTestSuite) TestUbuntuArchitecture(c *C) {
+ c.Check(ubuntuArchFromGoArch("386"), Equals, "i386")
+ c.Check(ubuntuArchFromGoArch("amd64"), Equals, "amd64")
+ c.Check(ubuntuArchFromGoArch("arm"), Equals, "armhf")
+ c.Check(ubuntuArchFromGoArch("arm64"), Equals, "arm64")
+ c.Check(ubuntuArchFromGoArch("ppc64le"), Equals, "ppc64el")
+}
+
+func (ts *ArchTestSuite) TestSetArchitecture(c *C) {
+ SetArchitecture("armhf")
+ c.Assert(UbuntuArchitecture(), Equals, "armhf")
+}
+
+func (ts *ArchTestSuite) TestSupportedArchitectures(c *C) {
+ arch = "armhf"
+ c.Check(IsSupportedArchitecture([]string{"all"}), Equals, true)
+ c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true)
+ c.Check(IsSupportedArchitecture([]string{"armhf"}), Equals, true)
+ c.Check(IsSupportedArchitecture([]string{"amd64", "powerpc"}), Equals, false)
+
+ arch = "amd64"
+ c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true)
+ c.Check(IsSupportedArchitecture([]string{"powerpc"}), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "time"
+)
+
+var (
+ accountValidationCertified = "certified"
+)
+
+// Account holds an account assertion, which ties a name for an account
+// to its identifier and provides the authority's confidence in the name's validity.
+type Account struct {
+ assertionBase
+ certified bool
+ timestamp time.Time
+}
+
+// AccountID returns the account-id of the account.
+func (acc *Account) AccountID() string {
+ return acc.HeaderString("account-id")
+}
+
+// Username returns the user name for the account.
+func (acc *Account) Username() string {
+ return acc.HeaderString("username")
+}
+
+// DisplayName returns the human-friendly name for the account.
+func (acc *Account) DisplayName() string {
+ return acc.HeaderString("display-name")
+}
+
+// IsCertified returns true if the authority has confidence in the account's name.
+func (acc *Account) IsCertified() bool {
+ return acc.certified
+}
+
+// Timestamp returns the time when the account was issued.
+func (acc *Account) Timestamp() time.Time {
+ return acc.timestamp
+}
+
+// Implement further consistency checks.
+func (acc *Account) checkConsistency(db RODatabase, acck *AccountKey) error {
+ if !db.IsTrustedAccount(acc.AuthorityID()) {
+ return fmt.Errorf("account assertion for %q is not signed by a directly trusted authority: %s", acc.AccountID(), acc.AuthorityID())
+ }
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*Account)(nil)
+
+func assembleAccount(assert assertionBase) (Assertion, error) {
+ _, err := checkNotEmptyString(assert.headers, "display-name")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "validation")
+ if err != nil {
+ return nil, err
+ }
+ certified := assert.headers["validation"] == accountValidationCertified
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkOptionalString(assert.headers, "username")
+ if err != nil {
+ return nil, err
+ }
+
+ return &Account{
+ assertionBase: assert,
+ certified: certified,
+ timestamp: timestamp,
+ }, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "regexp"
+ "time"
+)
+
+var validAccountKeyName = regexp.MustCompile(`^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$`)
+
+// AccountKey holds an account-key assertion, asserting a public key
+// belonging to the account.
+type AccountKey struct {
+ assertionBase
+ since time.Time
+ until time.Time
+ pubKey PublicKey
+}
+
+// AccountID returns the account-id of this account-key.
+func (ak *AccountKey) AccountID() string {
+ return ak.HeaderString("account-id")
+}
+
+// Name returns the name of the account key.
+func (ak *AccountKey) Name() string {
+ return ak.HeaderString("name")
+}
+
+func IsValidAccountKeyName(name string) bool {
+ return validAccountKeyName.MatchString(name)
+}
+
+// Since returns the time when the account key starts being valid.
+func (ak *AccountKey) Since() time.Time {
+ return ak.since
+}
+
+// Until returns the time when the account key stops being valid. A zero time means the key is valid forever.
+func (ak *AccountKey) Until() time.Time {
+ return ak.until
+}
+
+// PublicKeyID returns the key id used for lookup of the account key.
+func (ak *AccountKey) PublicKeyID() string {
+ return ak.pubKey.ID()
+}
+
+// isKeyValidAt returns whether the account key is valid at 'when' time.
+func (ak *AccountKey) isKeyValidAt(when time.Time) bool {
+ valid := when.After(ak.since) || when.Equal(ak.since)
+ if valid && !ak.until.IsZero() {
+ valid = when.Before(ak.until)
+ }
+ return valid
+}
+
+// publicKey returns the underlying public key of the account key.
+func (ak *AccountKey) publicKey() PublicKey {
+ return ak.pubKey
+}
+
+func checkPublicKey(ab *assertionBase, keyIDName string) (PublicKey, error) {
+ pubKey, err := DecodePublicKey(ab.Body())
+ if err != nil {
+ return nil, err
+ }
+ keyID, err := checkNotEmptyString(ab.headers, keyIDName)
+ if err != nil {
+ return nil, err
+ }
+ if keyID != pubKey.ID() {
+ return nil, fmt.Errorf("public key does not match provided key id")
+ }
+ return pubKey, nil
+}
+
+// Implement further consistency checks.
+func (ak *AccountKey) checkConsistency(db RODatabase, acck *AccountKey) error {
+ if !db.IsTrustedAccount(ak.AuthorityID()) {
+ return fmt.Errorf("account-key assertion for %q is not signed by a directly trusted authority: %s", ak.AccountID(), ak.AuthorityID())
+ }
+ _, err := db.Find(AccountType, map[string]string{
+ "account-id": ak.AccountID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("account-key assertion for %q does not have a matching account assertion", ak.AccountID())
+ }
+ if err != nil {
+ return err
+ }
+ // XXX: Make this unconditional once account-key assertions are required to have a name.
+ if ak.Name() != "" {
+ // Check that we don't end up with multiple keys with
+ // different IDs but the same account-id and name.
+ // Note that this is a non-transactional check-then-add, so
+ // is not a hard guarantee. Backstores that can implement a
+ // unique constraint should do so.
+ assertions, err := db.FindMany(AccountKeyType, map[string]string{
+ "account-id": ak.AccountID(),
+ "name": ak.Name(),
+ })
+ if err != nil && err != ErrNotFound {
+ return err
+ }
+ for _, assertion := range assertions {
+ existingAccKey := assertion.(*AccountKey)
+ if ak.PublicKeyID() != existingAccKey.PublicKeyID() {
+ return fmt.Errorf("account-key assertion for %q with ID %q has the same name %q as existing ID %q", ak.AccountID(), ak.PublicKeyID(), ak.Name(), existingAccKey.PublicKeyID())
+ }
+ }
+ }
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*AccountKey)(nil)
+
+// Prerequisites returns references to this account-key's prerequisite assertions.
+func (ak *AccountKey) Prerequisites() []*Ref {
+ return []*Ref{
+ {Type: AccountType, PrimaryKey: []string{ak.AccountID()}},
+ }
+}
+
+func assembleAccountKey(assert assertionBase) (Assertion, error) {
+ _, err := checkNotEmptyString(assert.headers, "account-id")
+ if err != nil {
+ return nil, err
+ }
+
+ // XXX: We should require name to be present after backfilling existing assertions.
+ _, ok := assert.headers["name"]
+ if ok {
+ _, err = checkStringMatches(assert.headers, "name", validAccountKeyName)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ since, err := checkRFC3339Date(assert.headers, "since")
+ if err != nil {
+ return nil, err
+ }
+
+ until, err := checkRFC3339DateWithDefault(assert.headers, "until", time.Time{})
+ if err != nil {
+ return nil, err
+ }
+ if !until.IsZero() && until.Before(since) {
+ return nil, fmt.Errorf("'until' time cannot be before 'since' time")
+ }
+
+ pubk, err := checkPublicKey(&assert, "public-key-sha3-384")
+ if err != nil {
+ return nil, err
+ }
+
+ // ignore extra headers for future compatibility
+ return &AccountKey{
+ assertionBase: assert,
+ since: since,
+ until: until,
+ pubKey: pubk,
+ }, nil
+}
+
+// AccountKeyRequest holds an account-key-request assertion, which is a self-signed request to prove that the requester holds the private key and wishes to create an account-key assertion for it.
+type AccountKeyRequest struct {
+ assertionBase
+ since time.Time
+ until time.Time
+ pubKey PublicKey
+}
+
+// AccountID returns the account-id of this account-key-request.
+func (akr *AccountKeyRequest) AccountID() string {
+ return akr.HeaderString("account-id")
+}
+
+// Name returns the name of the account key.
+func (akr *AccountKeyRequest) Name() string {
+ return akr.HeaderString("name")
+}
+
+// Since returns the time when the requested account key starts being valid.
+func (akr *AccountKeyRequest) Since() time.Time {
+ return akr.since
+}
+
+// Until returns the time when the requested account key stops being valid. A zero time means the key is valid forever.
+func (akr *AccountKeyRequest) Until() time.Time {
+ return akr.until
+}
+
+// PublicKeyID returns the underlying public key ID of the requested account key.
+func (akr *AccountKeyRequest) PublicKeyID() string {
+ return akr.pubKey.ID()
+}
+
+// signKey returns the underlying public key of the requested account key.
+func (akr *AccountKeyRequest) signKey() PublicKey {
+ return akr.pubKey
+}
+
+// Implement further consistency checks.
+func (akr *AccountKeyRequest) checkConsistency(db RODatabase, acck *AccountKey) error {
+ _, err := db.Find(AccountType, map[string]string{
+ "account-id": akr.AccountID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("account-key-request assertion for %q does not have a matching account assertion", akr.AccountID())
+ }
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// sanity
+var (
+ _ consistencyChecker = (*AccountKeyRequest)(nil)
+ _ customSigner = (*AccountKeyRequest)(nil)
+)
+
+// Prerequisites returns references to this account-key-request's prerequisite assertions.
+func (akr *AccountKeyRequest) Prerequisites() []*Ref {
+ return []*Ref{
+ {Type: AccountType, PrimaryKey: []string{akr.AccountID()}},
+ }
+}
+
+func assembleAccountKeyRequest(assert assertionBase) (Assertion, error) {
+ _, err := checkNotEmptyString(assert.headers, "account-id")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkStringMatches(assert.headers, "name", validAccountKeyName)
+ if err != nil {
+ return nil, err
+ }
+
+ since, err := checkRFC3339Date(assert.headers, "since")
+ if err != nil {
+ return nil, err
+ }
+
+ until, err := checkRFC3339DateWithDefault(assert.headers, "until", time.Time{})
+ if err != nil {
+ return nil, err
+ }
+ if !until.IsZero() && until.Before(since) {
+ return nil, fmt.Errorf("'until' time cannot be before 'since' time")
+ }
+
+ pubk, err := checkPublicKey(&assert, "public-key-sha3-384")
+ if err != nil {
+ return nil, err
+ }
+
+ // ignore extra headers for future compatibility
+ return &AccountKeyRequest{
+ assertionBase: assert,
+ since: since,
+ until: until,
+ pubKey: pubk,
+ }, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "encoding/base64"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+type accountKeySuite struct {
+ privKey asserts.PrivateKey
+ pubKeyBody string
+ keyID string
+ since, until time.Time
+ sinceLine, untilLine string
+}
+
+var _ = Suite(&accountKeySuite{})
+
+func (aks *accountKeySuite) SetUpSuite(c *C) {
+ cfg1 := &asserts.DatabaseConfig{}
+ accDb, err := asserts.OpenDatabase(cfg1)
+ c.Assert(err, IsNil)
+ aks.privKey = testPrivKey1
+ err = accDb.ImportKey(aks.privKey)
+ c.Assert(err, IsNil)
+ aks.keyID = aks.privKey.PublicKey().ID()
+
+ pubKey, err := accDb.PublicKey(aks.keyID)
+ c.Assert(err, IsNil)
+ pubKeyEncoded, err := asserts.EncodePublicKey(pubKey)
+ c.Assert(err, IsNil)
+ aks.pubKeyBody = string(pubKeyEncoded)
+
+ aks.since, err = time.Parse(time.RFC822, "16 Nov 15 15:04 UTC")
+ c.Assert(err, IsNil)
+ aks.until = aks.since.AddDate(1, 0, 0)
+ aks.sinceLine = "since: " + aks.since.Format(time.RFC3339) + "\n"
+ aks.untilLine = "until: " + aks.until.Format(time.RFC3339) + "\n"
+}
+
+func (aks *accountKeySuite) TestDecodeOK(c *C) {
+ encoded := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.AccountKeyType)
+ accKey := a.(*asserts.AccountKey)
+ c.Check(accKey.AccountID(), Equals, "acc-id1")
+ c.Check(accKey.Name(), Equals, "default")
+ c.Check(accKey.PublicKeyID(), Equals, aks.keyID)
+ c.Check(accKey.Since(), Equals, aks.since)
+}
+
+func (aks *accountKeySuite) TestDecodeNoName(c *C) {
+ // XXX: remove this test once name is mandatory
+ encoded := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.AccountKeyType)
+ accKey := a.(*asserts.AccountKey)
+ c.Check(accKey.AccountID(), Equals, "acc-id1")
+ c.Check(accKey.Name(), Equals, "")
+ c.Check(accKey.PublicKeyID(), Equals, aks.keyID)
+ c.Check(accKey.Since(), Equals, aks.since)
+}
+
+func (aks *accountKeySuite) TestUntil(c *C) {
+
+ untilSinceLine := "until: " + aks.since.Format(time.RFC3339) + "\n"
+
+ tests := []struct {
+ untilLine string
+ until time.Time
+ }{
+ {"", time.Time{}}, // zero time default
+ {aks.untilLine, aks.until}, // in the future
+ {untilSinceLine, aks.since}, // same as since
+ }
+
+ for _, test := range tests {
+ c.Log(test)
+ encoded := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ test.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "openpgp c2ln"
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ accKey := a.(*asserts.AccountKey)
+ c.Check(accKey.Until(), Equals, test.until)
+ }
+}
+
+const (
+ accKeyErrPrefix = "assertion account-key: "
+ accKeyReqErrPrefix = "assertion account-key-request: "
+)
+
+func (aks *accountKeySuite) TestDecodeInvalidHeaders(c *C) {
+
+ encoded := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ aks.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+
+ untilPast := aks.since.AddDate(-1, 0, 0)
+ untilPastLine := "until: " + untilPast.Format(time.RFC3339) + "\n"
+
+ invalidHeaderTests := []struct{ original, invalid, expectedErr string }{
+ {"account-id: acc-id1\n", "", `"account-id" header is mandatory`},
+ {"account-id: acc-id1\n", "account-id: \n", `"account-id" header should not be empty`},
+ // XXX: enable this once name is mandatory
+ // {"name: default\n", "", `"name" header is mandatory`},
+ {"name: default\n", "name: \n", `"name" header should not be empty`},
+ {"name: default\n", "name: a b\n", `"name" header contains invalid characters: "a b"`},
+ {"name: default\n", "name: -default\n", `"name" header contains invalid characters: "-default"`},
+ {"name: default\n", "name: foo:bar\n", `"name" header contains invalid characters: "foo:bar"`},
+ {"name: default\n", "name: a--b\n", `"name" header contains invalid characters: "a--b"`},
+ {"name: default\n", "name: 42\n", `"name" header contains invalid characters: "42"`},
+ {"public-key-sha3-384: " + aks.keyID + "\n", "", `"public-key-sha3-384" header is mandatory`},
+ {"public-key-sha3-384: " + aks.keyID + "\n", "public-key-sha3-384: \n", `"public-key-sha3-384" header should not be empty`},
+ {aks.sinceLine, "", `"since" header is mandatory`},
+ {aks.sinceLine, "since: \n", `"since" header should not be empty`},
+ {aks.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`},
+ {aks.sinceLine, "since: \n", `"since" header should not be empty`},
+ {aks.untilLine, "until: \n", `"until" header is not a RFC3339 date: .*`},
+ {aks.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`},
+ {aks.untilLine, untilPastLine, `'until' time cannot be before 'since' time`},
+ }
+
+ for _, test := range invalidHeaderTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, accKeyErrPrefix+test.expectedErr)
+ }
+}
+
+func (aks *accountKeySuite) TestDecodeInvalidPublicKey(c *C) {
+ headers := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ aks.untilLine
+
+ raw, err := base64.StdEncoding.DecodeString(aks.pubKeyBody)
+ c.Assert(err, IsNil)
+ spurious := base64.StdEncoding.EncodeToString(append(raw, "gorp"...))
+
+ invalidPublicKeyTests := []struct{ body, expectedErr string }{
+ {"", "cannot decode public key: no data"},
+ {"==", "cannot decode public key: .*"},
+ {"stuff", "cannot decode public key: .*"},
+ {"AnNpZw==", "unsupported public key format version: 2"},
+ {"AUJST0tFTg==", "cannot decode public key: .*"},
+ {spurious, "public key has spurious trailing data"},
+ }
+
+ for _, test := range invalidPublicKeyTests {
+ invalid := headers +
+ fmt.Sprintf("body-length: %v", len(test.body)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ test.body + "\n\n" +
+ "AXNpZw=="
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, accKeyErrPrefix+test.expectedErr)
+ }
+}
+
+func (aks *accountKeySuite) TestDecodeKeyIDMismatch(c *C) {
+ invalid := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: aa\n" +
+ aks.sinceLine +
+ aks.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, accKeyErrPrefix+"public key does not match provided key id")
+}
+
+func (aks *accountKeySuite) openDB(c *C) *asserts.Database {
+ trustedKey := testPrivKey0
+
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, IsNil)
+ cfg := &asserts.DatabaseConfig{
+ Backstore: bs,
+ Trusted: []asserts.Assertion{
+ asserts.BootstrapAccountForTest("canonical"),
+ asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()),
+ },
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+ return db
+}
+
+func (aks *accountKeySuite) prereqAccount(c *C, db *asserts.Database) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "display-name": "Acct1",
+ "account-id": "acc-id1",
+ "username": "acc-id1",
+ "validation": "unproven",
+ "timestamp": aks.since.Format(time.RFC3339),
+ }
+ acct1, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, trustedKey)
+ c.Assert(err, IsNil)
+
+ // prereq
+ db.Add(acct1)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheck(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+
+ aks.prereqAccount(c, db)
+
+ err = db.Check(accKey)
+ c.Assert(err, IsNil)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheckNoAccount(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+
+ err = db.Check(accKey)
+ c.Assert(err, ErrorMatches, `account-key assertion for "acc-id1" does not have a matching account assertion`)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheckUntrustedAuthority(c *C) {
+ trustedKey := testPrivKey0
+
+ db := aks.openDB(c)
+ storeDB := assertstest.NewSigningDB("canonical", trustedKey)
+ otherDB := setup3rdPartySigning(c, "other", storeDB, db)
+
+ headers := map[string]interface{}{
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := otherDB.Sign(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(accKey)
+ c.Assert(err, ErrorMatches, `account-key assertion for "acc-id1" is not signed by a directly trusted authority:.*`)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheckSameNameAndNewRevision(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+ aks.prereqAccount(c, db)
+
+ err = db.Add(accKey)
+ c.Assert(err, IsNil)
+
+ headers["revision"] = "1"
+ newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ err = db.Check(newAccKey)
+ c.Assert(err, IsNil)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheckSameAccountAndDifferentName(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+ aks.prereqAccount(c, db)
+
+ err = db.Add(accKey)
+ c.Assert(err, IsNil)
+
+ newPrivKey, _ := assertstest.GenerateKey(752)
+ err = db.ImportKey(newPrivKey)
+ c.Assert(err, IsNil)
+ newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID())
+ c.Assert(err, IsNil)
+ newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey)
+ c.Assert(err, IsNil)
+
+ headers["name"] = "another"
+ headers["public-key-sha3-384"] = newPubKey.ID()
+ newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey)
+ c.Assert(err, IsNil)
+
+ err = db.Check(newAccKey)
+ c.Assert(err, IsNil)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheckSameNameAndDifferentAccount(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+ err = db.ImportKey(trustedKey)
+ c.Assert(err, IsNil)
+ aks.prereqAccount(c, db)
+
+ err = db.Add(accKey)
+ c.Assert(err, IsNil)
+
+ newPrivKey, _ := assertstest.GenerateKey(752)
+ err = db.ImportKey(newPrivKey)
+ c.Assert(err, IsNil)
+ newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID())
+ c.Assert(err, IsNil)
+ newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey)
+ c.Assert(err, IsNil)
+
+ acct2 := assertstest.NewAccount(db, "acc-id2", map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id2",
+ }, trustedKey.PublicKey().ID())
+ db.Add(acct2)
+
+ headers["account-id"] = "acc-id2"
+ headers["public-key-sha3-384"] = newPubKey.ID()
+ headers["revision"] = "1"
+ newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey)
+ c.Assert(err, IsNil)
+
+ err = db.Check(newAccKey)
+ c.Assert(err, IsNil)
+}
+
+func (aks *accountKeySuite) TestAccountKeyCheckNameClash(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+ aks.prereqAccount(c, db)
+
+ err = db.Add(accKey)
+ c.Assert(err, IsNil)
+
+ newPrivKey, _ := assertstest.GenerateKey(752)
+ err = db.ImportKey(newPrivKey)
+ c.Assert(err, IsNil)
+ newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID())
+ c.Assert(err, IsNil)
+ newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey)
+ c.Assert(err, IsNil)
+
+ headers["public-key-sha3-384"] = newPubKey.ID()
+ headers["revision"] = "1"
+ newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey)
+ c.Assert(err, IsNil)
+
+ err = db.Check(newAccKey)
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`account-key assertion for "acc-id1" with ID %q has the same name "default" as existing ID %q`, newPubKey.ID(), aks.keyID))
+}
+
+func (aks *accountKeySuite) TestAccountKeyAddAndFind(c *C) {
+ trustedKey := testPrivKey0
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ "until": aks.until.Format(time.RFC3339),
+ }
+ accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+
+ aks.prereqAccount(c, db)
+
+ err = db.Add(accKey)
+ c.Assert(err, IsNil)
+
+ found, err := db.Find(asserts.AccountKeyType, map[string]string{
+ "account-id": "acc-id1",
+ "public-key-sha3-384": aks.keyID,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(found, NotNil)
+ c.Check(found.Body(), DeepEquals, []byte(aks.pubKeyBody))
+}
+
+func (aks *accountKeySuite) TestPublicKeyIsValidAt(c *C) {
+ // With since and until, i.e. signing account-key expires.
+ encoded := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ aks.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ accKey := a.(*asserts.AccountKey)
+
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, true)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, true)
+
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, -1, 0)), Equals, true)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, 1, 0)), Equals, false)
+
+ // With no until, i.e. signing account-key never expires.
+ encoded = "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "openpgp c2ln"
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ accKey = a.(*asserts.AccountKey)
+
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, true)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, true)
+
+ // With since == until, i.e. signing account-key has been revoked.
+ encoded = "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ "until: " + aks.since.Format(time.RFC3339) + "\n" +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "openpgp c2ln"
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ accKey = a.(*asserts.AccountKey)
+
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, false)
+
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, -1, 0)), Equals, false)
+ c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, 1, 0)), Equals, false)
+}
+
+func (aks *accountKeySuite) TestPrerequisites(c *C) {
+ encoded := "type: account-key\n" +
+ "authority-id: canonical\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ aks.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ prereqs := a.Prerequisites()
+ c.Assert(prereqs, HasLen, 1)
+ c.Check(prereqs[0], DeepEquals, &asserts.Ref{
+ Type: asserts.AccountType,
+ PrimaryKey: []string{"acc-id1"},
+ })
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestHappy(c *C) {
+ akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType,
+ map[string]interface{}{
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ }, []byte(aks.pubKeyBody), aks.privKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(akr))
+ c.Assert(err, IsNil)
+
+ akr2, ok := a.(*asserts.AccountKeyRequest)
+ c.Assert(ok, Equals, true)
+
+ db := aks.openDB(c)
+ aks.prereqAccount(c, db)
+
+ err = db.Check(akr2)
+ c.Check(err, IsNil)
+
+ c.Check(akr2.AccountID(), Equals, "acc-id1")
+ c.Check(akr2.Name(), Equals, "default")
+ c.Check(akr2.PublicKeyID(), Equals, aks.keyID)
+ c.Check(akr2.Since(), Equals, aks.since)
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestUntil(c *C) {
+ db := aks.openDB(c)
+ aks.prereqAccount(c, db)
+
+ tests := []struct {
+ untilHeader string
+ until time.Time
+ }{
+ {"", time.Time{}}, // zero time default
+ {aks.until.Format(time.RFC3339), aks.until}, // in the future
+ {aks.since.Format(time.RFC3339), aks.since}, // same as since
+ }
+
+ for _, test := range tests {
+ c.Log(test)
+ headers := map[string]interface{}{
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ }
+ if test.untilHeader != "" {
+ headers["until"] = test.untilHeader
+ }
+ akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, []byte(aks.pubKeyBody), aks.privKey)
+ c.Assert(err, IsNil)
+ a, err := asserts.Decode(asserts.Encode(akr))
+ c.Assert(err, IsNil)
+ akr2 := a.(*asserts.AccountKeyRequest)
+ c.Check(akr2.Until(), Equals, test.until)
+ err = db.Check(akr2)
+ c.Check(err, IsNil)
+ }
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestAddAndFind(c *C) {
+ akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType,
+ map[string]interface{}{
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ }, []byte(aks.pubKeyBody), aks.privKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+ aks.prereqAccount(c, db)
+
+ err = db.Add(akr)
+ c.Assert(err, IsNil)
+
+ found, err := db.Find(asserts.AccountKeyRequestType, map[string]string{
+ "account-id": "acc-id1",
+ "public-key-sha3-384": aks.keyID,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(found, NotNil)
+ c.Check(found.Body(), DeepEquals, []byte(aks.pubKeyBody))
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestDecodeInvalid(c *C) {
+ encoded := "type: account-key-request\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ aks.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+
+ untilPast := aks.since.AddDate(-1, 0, 0)
+ untilPastLine := "until: " + untilPast.Format(time.RFC3339) + "\n"
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"account-id: acc-id1\n", "", `"account-id" header is mandatory`},
+ {"account-id: acc-id1\n", "account-id: \n", `"account-id" header should not be empty`},
+ {"name: default\n", "", `"name" header is mandatory`},
+ {"name: default\n", "name: \n", `"name" header should not be empty`},
+ {"name: default\n", "name: a b\n", `"name" header contains invalid characters: "a b"`},
+ {"name: default\n", "name: -default\n", `"name" header contains invalid characters: "-default"`},
+ {"name: default\n", "name: foo:bar\n", `"name" header contains invalid characters: "foo:bar"`},
+ {"public-key-sha3-384: " + aks.keyID + "\n", "", `"public-key-sha3-384" header is mandatory`},
+ {"public-key-sha3-384: " + aks.keyID + "\n", "public-key-sha3-384: \n", `"public-key-sha3-384" header should not be empty`},
+ {aks.sinceLine, "", `"since" header is mandatory`},
+ {aks.sinceLine, "since: \n", `"since" header should not be empty`},
+ {aks.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`},
+ {aks.sinceLine, "since: \n", `"since" header should not be empty`},
+ {aks.untilLine, "until: \n", `"until" header is not a RFC3339 date: .*`},
+ {aks.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`},
+ {aks.untilLine, untilPastLine, `'until' time cannot be before 'since' time`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, accKeyReqErrPrefix+test.expectedErr)
+ }
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestDecodeInvalidPublicKey(c *C) {
+ headers := "type: account-key-request\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: " + aks.keyID + "\n" +
+ aks.sinceLine +
+ aks.untilLine
+
+ raw, err := base64.StdEncoding.DecodeString(aks.pubKeyBody)
+ c.Assert(err, IsNil)
+ spurious := base64.StdEncoding.EncodeToString(append(raw, "gorp"...))
+
+ invalidPublicKeyTests := []struct{ body, expectedErr string }{
+ {"", "cannot decode public key: no data"},
+ {"==", "cannot decode public key: .*"},
+ {"stuff", "cannot decode public key: .*"},
+ {"AnNpZw==", "unsupported public key format version: 2"},
+ {"AUJST0tFTg==", "cannot decode public key: .*"},
+ {spurious, "public key has spurious trailing data"},
+ }
+
+ for _, test := range invalidPublicKeyTests {
+ invalid := headers +
+ fmt.Sprintf("body-length: %v", len(test.body)) + "\n" +
+ "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" +
+ test.body + "\n\n" +
+ "AXNpZw=="
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, accKeyReqErrPrefix+test.expectedErr)
+ }
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestDecodeKeyIDMismatch(c *C) {
+ invalid := "type: account-key-request\n" +
+ "account-id: acc-id1\n" +
+ "name: default\n" +
+ "public-key-sha3-384: aa\n" +
+ aks.sinceLine +
+ aks.untilLine +
+ fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" +
+ "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" +
+ aks.pubKeyBody + "\n\n" +
+ "AXNpZw=="
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, "assertion account-key-request: public key does not match provided key id")
+}
+
+func (aks *accountKeySuite) TestAccountKeyRequestNoAccount(c *C) {
+ headers := map[string]interface{}{
+ "account-id": "acc-id1",
+ "name": "default",
+ "public-key-sha3-384": aks.keyID,
+ "since": aks.since.Format(time.RFC3339),
+ }
+ akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, []byte(aks.pubKeyBody), aks.privKey)
+ c.Assert(err, IsNil)
+
+ db := aks.openDB(c)
+
+ err = db.Check(akr)
+ c.Assert(err, ErrorMatches, `account-key-request assertion for "acc-id1" does not have a matching account assertion`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ . "gopkg.in/check.v1"
+)
+
+var (
+ _ = Suite(&accountSuite{})
+)
+
+type accountSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+func (s *accountSuite) SetUpSuite(c *C) {
+ s.ts = time.Now().Truncate(time.Second).UTC()
+ s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n"
+}
+
+const accountExample = "type: account\n" +
+ "authority-id: canonical\n" +
+ "account-id: abc-123\n" +
+ "display-name: Nice User\n" +
+ "username: nice\n" +
+ "validation: certified\n" +
+ "TSLINE" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (s *accountSuite) TestDecodeOK(c *C) {
+ encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.AccountType)
+ account := a.(*asserts.Account)
+ c.Check(account.AuthorityID(), Equals, "canonical")
+ c.Check(account.Timestamp(), Equals, s.ts)
+ c.Check(account.AccountID(), Equals, "abc-123")
+ c.Check(account.DisplayName(), Equals, "Nice User")
+ c.Check(account.Username(), Equals, "nice")
+ c.Check(account.IsCertified(), Equals, true)
+}
+
+func (s *accountSuite) TestOptional(c *C) {
+ encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1)
+
+ tests := []struct{ original, replacement string }{
+ {"username: nice\n", ""},
+ {"username: nice\n", "username: \n"},
+ }
+
+ for _, test := range tests {
+ valid := strings.Replace(encoded, test.original, test.replacement, 1)
+ _, err := asserts.Decode([]byte(valid))
+ c.Check(err, IsNil)
+ }
+}
+
+func (s *accountSuite) TestIsCertified(c *C) {
+ tests := []struct {
+ value string
+ isCertified bool
+ }{
+ {"certified", true},
+ {"unproven", false},
+ {"nonsense", false},
+ }
+
+ template := strings.Replace(accountExample, "TSLINE", s.tsLine, 1)
+ for _, test := range tests {
+ encoded := strings.Replace(
+ template,
+ "validation: certified\n",
+ fmt.Sprintf("validation: %s\n", test.value),
+ 1,
+ )
+ assert, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ account := assert.(*asserts.Account)
+ c.Check(account.IsCertified(), Equals, test.isCertified)
+ }
+}
+
+const (
+ accountErrPrefix = "assertion account: "
+)
+
+func (s *accountSuite) TestDecodeInvalid(c *C) {
+ encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1)
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"account-id: abc-123\n", "", `"account-id" header is mandatory`},
+ {"account-id: abc-123\n", "account-id: \n", `"account-id" header should not be empty`},
+ {"display-name: Nice User\n", "", `"display-name" header is mandatory`},
+ {"display-name: Nice User\n", "display-name: \n", `"display-name" header should not be empty`},
+ {"username: nice\n", "username:\n - foo\n - bar\n", `"username" header must be a string`},
+ {"validation: certified\n", "", `"validation" header is mandatory`},
+ {"validation: certified\n", "validation: \n", `"validation" header should not be empty`},
+ {s.tsLine, "", `"timestamp" header is mandatory`},
+ {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, accountErrPrefix+test.expectedErr)
+ }
+}
+
+func (s *accountSuite) TestCheckInconsistentTimestamp(c *C) {
+ ex, err := asserts.Decode([]byte(strings.Replace(accountExample, "TSLINE", s.tsLine, 1)))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ headers := ex.Headers()
+ headers["timestamp"] = "2011-01-01T14:00:00Z"
+ account, err := storeDB.Sign(asserts.AccountType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(account)
+ c.Assert(err, ErrorMatches, "account assertion timestamp outside of signing key validity")
+}
+
+func (s *accountSuite) TestCheckUntrustedAuthority(c *C) {
+ ex, err := asserts.Decode([]byte(strings.Replace(accountExample, "TSLINE", s.tsLine, 1)))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+ otherDB := setup3rdPartySigning(c, "other", storeDB, db)
+
+ headers := ex.Headers()
+ headers["timestamp"] = time.Now().Format(time.RFC3339)
+ account, err := otherDB.Sign(asserts.AccountType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(account)
+ c.Assert(err, ErrorMatches, `account assertion for "abc-123" is not signed by a directly trusted authority:.*`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "bufio"
+ "bytes"
+ "crypto"
+ "fmt"
+ "io"
+ "sort"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+)
+
+type typeFlags int
+
+const (
+ noAuthority typeFlags = iota + 1
+)
+
+// AssertionType describes a known assertion type with its name and metadata.
+type AssertionType struct {
+ // Name of the type.
+ Name string
+ // PrimaryKey holds the names of the headers that constitute the
+ // unique primary key for this assertion type.
+ PrimaryKey []string
+
+ assembler func(assert assertionBase) (Assertion, error)
+ flags typeFlags
+}
+
+// MaxSupportedFormat returns the maximum supported format iteration for the type.
+func (at *AssertionType) MaxSupportedFormat() int {
+ return maxSupportedFormat[at.Name]
+}
+
+// Understood assertion types.
+var (
+ AccountType = &AssertionType{"account", []string{"account-id"}, assembleAccount, 0}
+ AccountKeyType = &AssertionType{"account-key", []string{"public-key-sha3-384"}, assembleAccountKey, 0}
+ ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel, 0}
+ SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial, 0}
+ BaseDeclarationType = &AssertionType{"base-declaration", []string{"series"}, assembleBaseDeclaration, 0}
+ SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0}
+ SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0}
+ SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0}
+ SystemUserType = &AssertionType{"system-user", []string{"brand-id", "email"}, assembleSystemUser, 0}
+ ValidationType = &AssertionType{"validation", []string{"series", "snap-id", "approved-snap-id", "approved-snap-revision"}, assembleValidation, 0}
+
+// ...
+)
+
+// Assertion types without a definite authority set (on the wire and/or self-signed).
+var (
+ DeviceSessionRequestType = &AssertionType{"device-session-request", []string{"brand-id", "model", "serial"}, assembleDeviceSessionRequest, noAuthority}
+ SerialRequestType = &AssertionType{"serial-request", nil, assembleSerialRequest, noAuthority}
+ AccountKeyRequestType = &AssertionType{"account-key-request", []string{"public-key-sha3-384"}, assembleAccountKeyRequest, noAuthority}
+)
+
+var typeRegistry = map[string]*AssertionType{
+ AccountType.Name: AccountType,
+ AccountKeyType.Name: AccountKeyType,
+ ModelType.Name: ModelType,
+ SerialType.Name: SerialType,
+ BaseDeclarationType.Name: BaseDeclarationType,
+ SnapDeclarationType.Name: SnapDeclarationType,
+ SnapBuildType.Name: SnapBuildType,
+ SnapRevisionType.Name: SnapRevisionType,
+ SystemUserType.Name: SystemUserType,
+ ValidationType.Name: ValidationType,
+ // no authority
+ DeviceSessionRequestType.Name: DeviceSessionRequestType,
+ SerialRequestType.Name: SerialRequestType,
+ AccountKeyRequestType.Name: AccountKeyRequestType,
+}
+
+var maxSupportedFormat = map[string]int{}
+
+func init() {
+ // register maxSupportedFormats while breaking initialisation loop
+ maxSupportedFormat[SnapDeclarationType.Name] = 1
+}
+
+func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) {
+ prev := maxSupportedFormat[assertType.Name]
+ maxSupportedFormat[assertType.Name] = maxFormat
+ return func() {
+ maxSupportedFormat[assertType.Name] = prev
+ }
+}
+
+// Type returns the AssertionType with name or nil
+func Type(name string) *AssertionType {
+ return typeRegistry[name]
+}
+
+// Ref expresses a reference to an assertion.
+type Ref struct {
+ Type *AssertionType
+ PrimaryKey []string
+}
+
+func (ref *Ref) String() string {
+ pkStr := "-"
+ n := len(ref.Type.PrimaryKey)
+ if n != len(ref.PrimaryKey) {
+ pkStr = "???"
+ } else if n > 0 {
+ pkStr = ref.PrimaryKey[n-1]
+ if n > 1 {
+ sfx := []string{pkStr + ";"}
+ for i, k := range ref.Type.PrimaryKey[:n-1] {
+ sfx = append(sfx, fmt.Sprintf("%s:%s", k, ref.PrimaryKey[i]))
+ }
+ pkStr = strings.Join(sfx, " ")
+ }
+ }
+ return fmt.Sprintf("%s (%s)", ref.Type.Name, pkStr)
+}
+
+// Unique returns a unique string representing the reference that can be used as a key in maps.
+func (ref *Ref) Unique() string {
+ return fmt.Sprintf("%s/%s", ref.Type.Name, strings.Join(ref.PrimaryKey, "/"))
+}
+
+// Resolve resolves the reference using the given find function.
+func (ref *Ref) Resolve(find func(assertType *AssertionType, headers map[string]string) (Assertion, error)) (Assertion, error) {
+ if len(ref.PrimaryKey) != len(ref.Type.PrimaryKey) {
+ return nil, fmt.Errorf("%q assertion reference primary key has the wrong length (expected %v): %v", ref.Type.Name, ref.Type.PrimaryKey, ref.PrimaryKey)
+ }
+ headers := make(map[string]string, len(ref.PrimaryKey))
+ for i, name := range ref.Type.PrimaryKey {
+ headers[name] = ref.PrimaryKey[i]
+ }
+ return find(ref.Type, headers)
+}
+
+// Assertion represents an assertion through its general elements.
+type Assertion interface {
+ // Type returns the type of this assertion
+ Type() *AssertionType
+ // Format returns the format iteration of this assertion
+ Format() int
+ // SupportedFormat returns whether the assertion uses a supported
+ // format iteration. If false the assertion might have been only
+ // partially parsed.
+ SupportedFormat() bool
+ // Revision returns the revision of this assertion
+ Revision() int
+ // AuthorityID returns the authority that signed this assertion
+ AuthorityID() string
+
+ // Header retrieves the header with name
+ Header(name string) interface{}
+
+ // Headers returns the complete headers
+ Headers() map[string]interface{}
+
+ // HeaderString retrieves the string value of header with name or ""
+ HeaderString(name string) string
+
+ // Body returns the body of this assertion
+ Body() []byte
+
+ // Signature returns the signed content and its unprocessed signature
+ Signature() (content, signature []byte)
+
+ // SignKeyID returns the key id for the key that signed this assertion.
+ SignKeyID() string
+
+ // Prerequisites returns references to the prerequisite assertions for the validity of this one.
+ Prerequisites() []*Ref
+
+ // Ref returns a reference representing this assertion.
+ Ref() *Ref
+}
+
+// customSigner represents an assertion with special arrangements for its signing key (e.g. self-signed), rather than the usual case where an assertion is signed by its authority.
+type customSigner interface {
+ // signKey returns the public key material for the key that signed this assertion. See also SignKeyID.
+ signKey() PublicKey
+}
+
+// MediaType is the media type for encoded assertions on the wire.
+const MediaType = "application/x.ubuntu.assertion"
+
+// assertionBase is the concrete base to hold representation data for actual assertions.
+type assertionBase struct {
+ headers map[string]interface{}
+ body []byte
+ // parsed format iteration
+ format int
+ // parsed revision
+ revision int
+ // preserved content
+ content []byte
+ // unprocessed signature
+ signature []byte
+}
+
+// HeaderString retrieves the string value of header with name or ""
+func (ab *assertionBase) HeaderString(name string) string {
+ s, _ := ab.headers[name].(string)
+ return s
+}
+
+// Type returns the assertion type.
+func (ab *assertionBase) Type() *AssertionType {
+ return Type(ab.HeaderString("type"))
+}
+
+// Format returns the assertion format iteration.
+func (ab *assertionBase) Format() int {
+ return ab.format
+}
+
+// SupportedFormat returns whether the assertion uses a supported
+// format iteration. If false the assertion might have been only
+// partially parsed.
+func (ab *assertionBase) SupportedFormat() bool {
+ return ab.format <= maxSupportedFormat[ab.HeaderString("type")]
+}
+
+// Revision returns the assertion revision.
+func (ab *assertionBase) Revision() int {
+ return ab.revision
+}
+
+// AuthorityID returns the authority-id a.k.a the signer id of the assertion.
+func (ab *assertionBase) AuthorityID() string {
+ return ab.HeaderString("authority-id")
+}
+
+// Header returns the value of an header by name.
+func (ab *assertionBase) Header(name string) interface{} {
+ v := ab.headers[name]
+ if v == nil {
+ return nil
+ }
+ return copyHeader(v)
+}
+
+// Headers returns the complete headers.
+func (ab *assertionBase) Headers() map[string]interface{} {
+ return copyHeaders(ab.headers)
+}
+
+// Body returns the body of the assertion.
+func (ab *assertionBase) Body() []byte {
+ return ab.body
+}
+
+// Signature returns the signed content and its unprocessed signature.
+func (ab *assertionBase) Signature() (content, signature []byte) {
+ return ab.content, ab.signature
+}
+
+// SignKeyID returns the key id for the key that signed this assertion.
+func (ab *assertionBase) SignKeyID() string {
+ return ab.HeaderString("sign-key-sha3-384")
+}
+
+// Prerequisites returns references to the prerequisite assertions for the validity of this one.
+func (ab *assertionBase) Prerequisites() []*Ref {
+ return nil
+}
+
+// Ref returns a reference representing this assertion.
+func (ab *assertionBase) Ref() *Ref {
+ assertType := ab.Type()
+ primKey := make([]string, len(assertType.PrimaryKey))
+ for i, name := range assertType.PrimaryKey {
+ primKey[i] = ab.HeaderString(name)
+ }
+ return &Ref{
+ Type: assertType,
+ PrimaryKey: primKey,
+ }
+}
+
+// sanity check
+var _ Assertion = (*assertionBase)(nil)
+
+// Decode parses a serialized assertion.
+//
+// The expected serialisation format looks like:
+//
+// HEADER ("\n\n" BODY?)? "\n\n" SIGNATURE
+//
+// where:
+//
+// HEADER is a set of header entries separated by "\n"
+// BODY can be arbitrary,
+// SIGNATURE is the signature
+//
+// A header entry for a single line value (no '\n' in it) looks like:
+//
+// NAME ": " SIMPLEVALUE
+//
+// The format supports multiline text values (with '\n's in them) and
+// lists possibly nested with string scalars in them.
+//
+// For those a header entry looks like:
+//
+// NAME ":\n" MULTI(baseindent)
+//
+// where MULTI can be
+//
+// * (baseindent + 4)-space indented value (multiline text)
+// * entries of a list each of the form:
+//
+// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI )
+//
+// baseindent starts at 0 and then grows with nesting matching the
+// previous level introduction (the " "*baseindent " -" bit)
+// length minus 1.
+//
+// In general the following headers are mandatory:
+//
+// type
+// authority-id (except for on the wire/self-signed assertions like serial-request)
+//
+// Further for a given assertion type all the primary key headers
+// must be non empty and must not contain '/'.
+//
+// The following headers expect string representing integer values and
+// if omitted otherwise are assumed to be 0:
+//
+// revision (a positive int)
+// body-length (expected to be equal to the length of BODY)
+//
+// Times are expected to be in the RFC3339 format: "2006-01-02T15:04:05Z07:00".
+func Decode(serializedAssertion []byte) (Assertion, error) {
+ // copy to get an independent backstorage that can't be mutated later
+ assertionSnapshot := make([]byte, len(serializedAssertion))
+ copy(assertionSnapshot, serializedAssertion)
+ contentSignatureSplit := bytes.LastIndex(assertionSnapshot, nlnl)
+ if contentSignatureSplit == -1 {
+ return nil, fmt.Errorf("assertion content/signature separator not found")
+ }
+ content := assertionSnapshot[:contentSignatureSplit]
+ signature := assertionSnapshot[contentSignatureSplit+2:]
+
+ headersBodySplit := bytes.Index(content, nlnl)
+ var body, head []byte
+ if headersBodySplit == -1 {
+ head = content
+ } else {
+ body = content[headersBodySplit+2:]
+ if len(body) == 0 {
+ body = nil
+ }
+ head = content[:headersBodySplit]
+ }
+
+ headers, err := parseHeaders(head)
+ if err != nil {
+ return nil, fmt.Errorf("parsing assertion headers: %v", err)
+ }
+
+ return assemble(headers, body, content, signature)
+}
+
+// Maximum assertion component sizes.
+const (
+ MaxBodySize = 2 * 1024 * 1024
+ MaxHeadersSize = 128 * 1024
+ MaxSignatureSize = 128 * 1024
+)
+
+// Decoder parses a stream of assertions bundled by separating them with double newlines.
+type Decoder struct {
+ rd io.Reader
+ initialBufSize int
+ b *bufio.Reader
+ err error
+ maxHeadersSize int
+ maxBodySize int
+ maxSigSize int
+}
+
+// initBuffer finishes a Decoder initialization by setting up the bufio.Reader,
+// it returns the *Decoder for convenience of notation.
+func (d *Decoder) initBuffer() *Decoder {
+ d.b = bufio.NewReaderSize(d.rd, d.initialBufSize)
+ return d
+}
+
+const defaultDecoderButSize = 4096
+
+// NewDecoder returns a Decoder to parse the stream of assertions from the reader.
+func NewDecoder(r io.Reader) *Decoder {
+ return (&Decoder{
+ rd: r,
+ initialBufSize: defaultDecoderButSize,
+ maxHeadersSize: MaxHeadersSize,
+ maxBodySize: MaxBodySize,
+ maxSigSize: MaxSignatureSize,
+ }).initBuffer()
+}
+
+func (d *Decoder) peek(size int) ([]byte, error) {
+ buf, err := d.b.Peek(size)
+ if err == bufio.ErrBufferFull {
+ rebuf, reerr := d.b.Peek(d.b.Buffered())
+ if reerr != nil {
+ panic(reerr)
+ }
+ mr := io.MultiReader(bytes.NewBuffer(rebuf), d.rd)
+ d.b = bufio.NewReaderSize(mr, (size/d.initialBufSize+1)*d.initialBufSize)
+ buf, err = d.b.Peek(size)
+ }
+ if err != nil && d.err == nil {
+ d.err = err
+ }
+ return buf, d.err
+}
+
+// NB: readExact and readUntil use peek underneath and their returned
+// buffers are valid only until the next reading call
+
+func (d *Decoder) readExact(size int) ([]byte, error) {
+ buf, err := d.peek(size)
+ d.b.Discard(len(buf))
+ if len(buf) == size {
+ return buf, nil
+ }
+ if err == io.EOF {
+ return buf, io.ErrUnexpectedEOF
+ }
+ return buf, err
+}
+
+func (d *Decoder) readUntil(delim []byte, maxSize int) ([]byte, error) {
+ last := 0
+ size := d.initialBufSize
+ for {
+ buf, err := d.peek(size)
+ if i := bytes.Index(buf[last:], delim); i >= 0 {
+ d.b.Discard(last + i + len(delim))
+ return buf[:last+i+len(delim)], nil
+ }
+ // report errors only once we have consumed what is buffered
+ if err != nil && len(buf) == d.b.Buffered() {
+ d.b.Discard(len(buf))
+ return buf, err
+ }
+ last = size - len(delim) + 1
+ size *= 2
+ if size > maxSize {
+ return nil, fmt.Errorf("maximum size exceeded while looking for delimiter %q", delim)
+ }
+ }
+}
+
+// Decode parses the next assertion from the stream.
+// It returns the error io.EOF at the end of a well-formed stream.
+func (d *Decoder) Decode() (Assertion, error) {
+ // read the headers and the nlnl separator after them
+ headAndSep, err := d.readUntil(nlnl, d.maxHeadersSize)
+ if err != nil {
+ if err == io.EOF {
+ if len(headAndSep) != 0 {
+ return nil, io.ErrUnexpectedEOF
+ }
+ return nil, io.EOF
+ }
+ return nil, fmt.Errorf("error reading assertion headers: %v", err)
+ }
+
+ headLen := len(headAndSep) - len(nlnl)
+ headers, err := parseHeaders(headAndSep[:headLen])
+ if err != nil {
+ return nil, fmt.Errorf("parsing assertion headers: %v", err)
+ }
+
+ length, err := checkIntWithDefault(headers, "body-length", 0)
+ if err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+ if length > d.maxBodySize {
+ return nil, fmt.Errorf("assertion body length %d exceeds maximum body size", length)
+ }
+
+ // save the headers before we try to read more, and setup to capture
+ // the whole content in a buffer
+ contentBuf := bytes.NewBuffer(make([]byte, 0, len(headAndSep)+length))
+ contentBuf.Write(headAndSep)
+
+ if length > 0 {
+ // read the body if length != 0
+ body, err := d.readExact(length)
+ if err != nil {
+ return nil, err
+ }
+ contentBuf.Write(body)
+ }
+
+ // try to read the end of body a.k.a content/signature separator
+ endOfBody, err := d.readUntil(nlnl, d.maxSigSize)
+ if err != nil && err != io.EOF {
+ return nil, fmt.Errorf("error reading assertion trailer: %v", err)
+ }
+
+ var sig []byte
+ if bytes.Equal(endOfBody, nlnl) {
+ // we got the nlnl content/signature separator, read the signature now and the assertion/assertion nlnl separation
+ sig, err = d.readUntil(nlnl, d.maxSigSize)
+ if err != nil && err != io.EOF {
+ return nil, fmt.Errorf("error reading assertion signature: %v", err)
+ }
+ } else {
+ // we got the signature directly which is a ok format only if body length == 0
+ if length > 0 {
+ return nil, fmt.Errorf("missing content/signature separator")
+ }
+ sig = endOfBody
+ contentBuf.Truncate(headLen)
+ }
+
+ // normalize sig ending newlines
+ if bytes.HasSuffix(sig, nlnl) {
+ sig = sig[:len(sig)-1]
+ }
+
+ finalContent := contentBuf.Bytes()
+ var finalBody []byte
+ if length > 0 {
+ finalBody = finalContent[headLen+len(nlnl):]
+ }
+
+ finalSig := make([]byte, len(sig))
+ copy(finalSig, sig)
+
+ return assemble(headers, finalBody, finalContent, finalSig)
+}
+
+func checkIteration(headers map[string]interface{}, name string) (int, error) {
+ iternum, err := checkIntWithDefault(headers, name, 0)
+ if err != nil {
+ return -1, err
+ }
+ if iternum < 0 {
+ return -1, fmt.Errorf("%s should be positive: %v", name, iternum)
+ }
+ return iternum, nil
+}
+
+func checkFormat(headers map[string]interface{}) (int, error) {
+ return checkIteration(headers, "format")
+}
+
+func checkRevision(headers map[string]interface{}) (int, error) {
+ return checkIteration(headers, "revision")
+}
+
+// Assemble assembles an assertion from its components.
+func Assemble(headers map[string]interface{}, body, content, signature []byte) (Assertion, error) {
+ err := checkHeaders(headers)
+ if err != nil {
+ return nil, err
+ }
+ return assemble(headers, body, content, signature)
+}
+
+// assemble is the internal variant of Assemble, assumes headers are already checked for supported types
+func assemble(headers map[string]interface{}, body, content, signature []byte) (Assertion, error) {
+ length, err := checkIntWithDefault(headers, "body-length", 0)
+ if err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+ if length != len(body) {
+ return nil, fmt.Errorf("assertion body length and declared body-length don't match: %v != %v", len(body), length)
+ }
+
+ if !utf8.Valid(body) {
+ return nil, fmt.Errorf("body is not utf8")
+ }
+
+ if _, err := checkDigest(headers, "sign-key-sha3-384", crypto.SHA3_384); err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+
+ typ, err := checkNotEmptyString(headers, "type")
+ if err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+ assertType := Type(typ)
+ if assertType == nil {
+ return nil, fmt.Errorf("unknown assertion type: %q", typ)
+ }
+
+ if assertType.flags&noAuthority == 0 {
+ if _, err := checkNotEmptyString(headers, "authority-id"); err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+ } else {
+ _, ok := headers["authority-id"]
+ if ok {
+ return nil, fmt.Errorf("%q assertion cannot have authority-id set", assertType.Name)
+ }
+ }
+
+ formatnum, err := checkFormat(headers)
+ if err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+
+ for _, primKey := range assertType.PrimaryKey {
+ if _, err := checkPrimaryKey(headers, primKey); err != nil {
+ return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err)
+ }
+ }
+
+ revision, err := checkRevision(headers)
+ if err != nil {
+ return nil, fmt.Errorf("assertion: %v", err)
+ }
+
+ if len(signature) == 0 {
+ return nil, fmt.Errorf("empty assertion signature")
+ }
+
+ assert, err := assertType.assembler(assertionBase{
+ headers: headers,
+ body: body,
+ format: formatnum,
+ revision: revision,
+ content: content,
+ signature: signature,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err)
+ }
+ return assert, nil
+}
+
+func writeHeader(buf *bytes.Buffer, headers map[string]interface{}, name string) {
+ appendEntry(buf, fmt.Sprintf("%s:", name), headers[name], 0)
+}
+
+func assembleAndSign(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) {
+ err := checkAssertType(assertType)
+ if err != nil {
+ return nil, err
+ }
+
+ withAuthority := assertType.flags&noAuthority == 0
+
+ err = checkHeaders(headers)
+ if err != nil {
+ return nil, err
+ }
+
+ // there's no hint at all that we will need non-textual bodies,
+ // make sure we actually enforce that
+ if !utf8.Valid(body) {
+ return nil, fmt.Errorf("assertion body is not utf8")
+ }
+
+ finalHeaders := copyHeaders(headers)
+ bodyLength := len(body)
+ finalBody := make([]byte, bodyLength)
+ copy(finalBody, body)
+ finalHeaders["type"] = assertType.Name
+ finalHeaders["body-length"] = strconv.Itoa(bodyLength)
+ finalHeaders["sign-key-sha3-384"] = privKey.PublicKey().ID()
+
+ if withAuthority {
+ if _, err := checkNotEmptyString(finalHeaders, "authority-id"); err != nil {
+ return nil, err
+ }
+ } else {
+ _, ok := finalHeaders["authority-id"]
+ if ok {
+ return nil, fmt.Errorf("%q assertion cannot have authority-id set", assertType.Name)
+ }
+ }
+
+ formatnum, err := checkFormat(finalHeaders)
+ if err != nil {
+ return nil, err
+ }
+
+ if formatnum > assertType.MaxSupportedFormat() {
+ return nil, fmt.Errorf("cannot sign %q assertion with format %d higher than max supported format %d", assertType.Name, formatnum, assertType.MaxSupportedFormat())
+ }
+
+ revision, err := checkRevision(finalHeaders)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := bytes.NewBufferString("type: ")
+ buf.WriteString(assertType.Name)
+
+ if formatnum > 0 {
+ writeHeader(buf, finalHeaders, "format")
+ } else {
+ delete(finalHeaders, "format")
+ }
+
+ if withAuthority {
+ writeHeader(buf, finalHeaders, "authority-id")
+ }
+
+ if revision > 0 {
+ writeHeader(buf, finalHeaders, "revision")
+ } else {
+ delete(finalHeaders, "revision")
+ }
+ written := map[string]bool{
+ "type": true,
+ "format": true,
+ "authority-id": true,
+ "revision": true,
+ "body-length": true,
+ "sign-key-sha3-384": true,
+ }
+ for _, primKey := range assertType.PrimaryKey {
+ if _, err := checkPrimaryKey(finalHeaders, primKey); err != nil {
+ return nil, err
+ }
+ writeHeader(buf, finalHeaders, primKey)
+ written[primKey] = true
+ }
+
+ // emit other headers in lexicographic order
+ otherKeys := make([]string, 0, len(finalHeaders))
+ for name := range finalHeaders {
+ if !written[name] {
+ otherKeys = append(otherKeys, name)
+ }
+ }
+ sort.Strings(otherKeys)
+ for _, k := range otherKeys {
+ writeHeader(buf, finalHeaders, k)
+ }
+
+ // body-length and body
+ if bodyLength > 0 {
+ writeHeader(buf, finalHeaders, "body-length")
+ } else {
+ delete(finalHeaders, "body-length")
+ }
+
+ // signing key reference
+ writeHeader(buf, finalHeaders, "sign-key-sha3-384")
+
+ if bodyLength > 0 {
+ buf.Grow(bodyLength + 2)
+ buf.Write(nlnl)
+ buf.Write(finalBody)
+ } else {
+ finalBody = nil
+ }
+ content := buf.Bytes()
+
+ signature, err := signContent(content, privKey)
+ if err != nil {
+ return nil, fmt.Errorf("cannot sign assertion: %v", err)
+ }
+ // be 'cat' friendly, add a ignored newline to the signature which is the last part of the encoded assertion
+ signature = append(signature, '\n')
+
+ assert, err := assertType.assembler(assertionBase{
+ headers: finalHeaders,
+ body: finalBody,
+ format: formatnum,
+ revision: revision,
+ content: content,
+ signature: signature,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("cannot assemble assertion %s: %v", assertType.Name, err)
+ }
+ return assert, nil
+}
+
+// SignWithoutAuthority assembles an assertion without a set authority with the provided information and signs it with the given private key.
+func SignWithoutAuthority(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) {
+ if assertType.flags&noAuthority == 0 {
+ return nil, fmt.Errorf("cannot sign assertions needing a definite authority with SignWithoutAuthority")
+ }
+ return assembleAndSign(assertType, headers, body, privKey)
+}
+
+// Encode serializes an assertion.
+func Encode(assert Assertion) []byte {
+ content, signature := assert.Signature()
+ needed := len(content) + 2 + len(signature)
+ buf := bytes.NewBuffer(make([]byte, 0, needed))
+ buf.Write(content)
+ buf.Write(nlnl)
+ buf.Write(signature)
+ return buf.Bytes()
+}
+
+// Encoder emits a stream of assertions bundled by separating them with double newlines.
+type Encoder struct {
+ wr io.Writer
+ nextSep []byte
+}
+
+// NewEncoder returns a Encoder to emit a stream of assertions to a writer.
+func NewEncoder(w io.Writer) *Encoder {
+ return &Encoder{wr: w}
+}
+
+// append emits an already encoded assertion into the stream with a proper required separator.
+func (enc *Encoder) append(encoded []byte) error {
+ sz := len(encoded)
+ if sz == 0 {
+ return fmt.Errorf("internal error: encoded assertion cannot be empty")
+ }
+
+ _, err := enc.wr.Write(enc.nextSep)
+ if err != nil {
+ return err
+ }
+
+ _, err = enc.wr.Write(encoded)
+ if err != nil {
+ return err
+ }
+
+ if encoded[sz-1] != '\n' {
+ _, err = enc.wr.Write(nl)
+ if err != nil {
+ return err
+ }
+ }
+ enc.nextSep = nl
+
+ return nil
+}
+
+// Encode emits the assertion into the stream with the required separator.
+// Errors here are always about writing given that Encode() itself cannot error.
+func (enc *Encoder) Encode(assert Assertion) error {
+ encoded := Encode(assert)
+ return enc.append(encoded)
+}
+
+// SignatureCheck checks the signature of the assertion against the given public key. Useful for assertions with no authority.
+func SignatureCheck(assert Assertion, pubKey PublicKey) error {
+ content, encodedSig := assert.Signature()
+ sig, err := decodeSignature(encodedSig)
+ if err != nil {
+ return err
+ }
+ err = pubKey.verify(content, sig)
+ if err != nil {
+ return fmt.Errorf("failed signature verification: %v", err)
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "bytes"
+ "io"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type assertsSuite struct{}
+
+var _ = Suite(&assertsSuite{})
+
+func (as *assertsSuite) TestType(c *C) {
+ c.Check(asserts.Type("test-only"), Equals, asserts.TestOnlyType)
+}
+
+func (as *assertsSuite) TestUnknown(c *C) {
+ c.Check(asserts.Type(""), IsNil)
+ c.Check(asserts.Type("unknown"), IsNil)
+}
+
+func (as *assertsSuite) TestTypeMaxSupportedFormat(c *C) {
+ c.Check(asserts.Type("test-only").MaxSupportedFormat(), Equals, 1)
+}
+
+func (as *assertsSuite) TestRef(c *C) {
+ ref := &asserts.Ref{
+ Type: asserts.TestOnly2Type,
+ PrimaryKey: []string{"abc", "xyz"},
+ }
+ c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz")
+}
+
+func (as *assertsSuite) TestRefString(c *C) {
+ ref := &asserts.Ref{
+ Type: asserts.AccountType,
+ PrimaryKey: []string{"canonical"},
+ }
+
+ c.Check(ref.String(), Equals, "account (canonical)")
+
+ ref = &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"18", "SNAPID"},
+ }
+
+ c.Check(ref.String(), Equals, "snap-declaration (SNAPID; series:18)")
+
+ ref = &asserts.Ref{
+ Type: asserts.ModelType,
+ PrimaryKey: []string{"18", "BRAND", "baz-3000"},
+ }
+
+ c.Check(ref.String(), Equals, "model (baz-3000; series:18 brand-id:BRAND)")
+
+ // broken primary key
+ ref = &asserts.Ref{
+ Type: asserts.ModelType,
+ PrimaryKey: []string{"18"},
+ }
+ c.Check(ref.String(), Equals, "model (???)")
+
+ ref = &asserts.Ref{
+ Type: asserts.TestOnlyNoAuthorityType,
+ }
+ c.Check(ref.String(), Equals, "test-only-no-authority (-)")
+}
+
+func (as *assertsSuite) TestRefResolveError(c *C) {
+ ref := &asserts.Ref{
+ Type: asserts.TestOnly2Type,
+ PrimaryKey: []string{"abc"},
+ }
+ _, err := ref.Resolve(nil)
+ c.Check(err, ErrorMatches, `"test-only-2" assertion reference primary key has the wrong length \(expected \[pk1 pk2\]\): \[abc\]`)
+}
+
+const exKeyID = "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij"
+
+const exampleEmptyBodyAllDefaults = "type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: abc\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (as *assertsSuite) TestDecodeEmptyBodyAllDefaults(c *C) {
+ a, err := asserts.Decode([]byte(exampleEmptyBodyAllDefaults))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.TestOnlyType)
+ _, ok := a.(*asserts.TestOnly)
+ c.Check(ok, Equals, true)
+ c.Check(a.Revision(), Equals, 0)
+ c.Check(a.Format(), Equals, 0)
+ c.Check(a.Body(), IsNil)
+ c.Check(a.Header("header1"), IsNil)
+ c.Check(a.HeaderString("header1"), Equals, "")
+ c.Check(a.AuthorityID(), Equals, "auth-id1")
+ c.Check(a.SignKeyID(), Equals, exKeyID)
+}
+
+const exampleEmptyBody2NlNl = "type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: xyz\n" +
+ "revision: 0\n" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "\n\n" +
+ "AXNpZw==\n"
+
+func (as *assertsSuite) TestDecodeEmptyBodyNormalize2NlNl(c *C) {
+ a, err := asserts.Decode([]byte(exampleEmptyBody2NlNl))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.TestOnlyType)
+ c.Check(a.Revision(), Equals, 0)
+ c.Check(a.Format(), Equals, 0)
+ c.Check(a.Body(), IsNil)
+}
+
+const exampleBodyAndExtraHeaders = "type: test-only\n" +
+ "format: 1\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: abc\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==\n"
+
+func (as *assertsSuite) TestDecodeWithABodyAndExtraHeaders(c *C) {
+ a, err := asserts.Decode([]byte(exampleBodyAndExtraHeaders))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.TestOnlyType)
+ c.Check(a.AuthorityID(), Equals, "auth-id2")
+ c.Check(a.SignKeyID(), Equals, exKeyID)
+ c.Check(a.Header("primary-key"), Equals, "abc")
+ c.Check(a.Revision(), Equals, 5)
+ c.Check(a.Format(), Equals, 1)
+ c.Check(a.SupportedFormat(), Equals, true)
+ c.Check(a.Header("header1"), Equals, "value1")
+ c.Check(a.Header("header2"), Equals, "value2")
+ c.Check(a.Body(), DeepEquals, []byte("THE-BODY"))
+
+}
+
+const exampleUnsupportedFormat = "type: test-only\n" +
+ "format: 77\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: abc\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "AXNpZw==\n"
+
+func (as *assertsSuite) TestDecodeUnsupportedFormat(c *C) {
+ a, err := asserts.Decode([]byte(exampleUnsupportedFormat))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.TestOnlyType)
+ c.Check(a.AuthorityID(), Equals, "auth-id2")
+ c.Check(a.SignKeyID(), Equals, exKeyID)
+ c.Check(a.Header("primary-key"), Equals, "abc")
+ c.Check(a.Revision(), Equals, 5)
+ c.Check(a.Format(), Equals, 77)
+ c.Check(a.SupportedFormat(), Equals, false)
+}
+
+func (as *assertsSuite) TestDecodeGetSignatureBits(c *C) {
+ content := "type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: xyz\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY"
+ encoded := content +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.TestOnlyType)
+ c.Check(a.AuthorityID(), Equals, "auth-id1")
+ c.Check(a.SignKeyID(), Equals, exKeyID)
+ cont, signature := a.Signature()
+ c.Check(signature, DeepEquals, []byte("AXNpZw=="))
+ c.Check(cont, DeepEquals, []byte(content))
+}
+
+func (as *assertsSuite) TestDecodeNoSignatureSplit(c *C) {
+ for _, encoded := range []string{"", "foo"} {
+ _, err := asserts.Decode([]byte(encoded))
+ c.Check(err, ErrorMatches, "assertion content/signature separator not found")
+ }
+}
+
+func (as *assertsSuite) TestDecodeHeaderParsingErrors(c *C) {
+ headerParsingErrorsTests := []struct{ encoded, expectedErr string }{
+ {string([]byte{255, '\n', '\n'}), "header is not utf8"},
+ {"foo: a\nbar\n\n", `header entry missing ':' separator: "bar"`},
+ {"TYPE: foo\n\n", `invalid header name: "TYPE"`},
+ {"foo: a\nbar:>\n\n", `header entry should have a space or newline \(for multiline\) before value: "bar:>"`},
+ {"foo: a\nbar:\n\n", `expected 4 chars nesting prefix after multiline introduction "bar:": EOF`},
+ {"foo: a\nbar:\nbaz: x\n\n", `expected 4 chars nesting prefix after multiline introduction "bar:": "baz: x"`},
+ {"foo: a:\nbar: b\nfoo: x\n\n", `repeated header: "foo"`},
+ }
+
+ for _, test := range headerParsingErrorsTests {
+ _, err := asserts.Decode([]byte(test.encoded))
+ c.Check(err, ErrorMatches, "parsing assertion headers: "+test.expectedErr)
+ }
+}
+
+func (as *assertsSuite) TestDecodeInvalid(c *C) {
+ keyIDHdr := "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n"
+ encoded := "type: test-only\n" +
+ "format: 0\n" +
+ "authority-id: auth-id\n" +
+ "primary-key: abc\n" +
+ "revision: 0\n" +
+ "body-length: 5\n" +
+ keyIDHdr +
+ "\n" +
+ "abcde" +
+ "\n\n" +
+ "AXNpZw=="
+
+ invalidAssertTests := []struct{ original, invalid, expectedErr string }{
+ {"body-length: 5", "body-length: z", `assertion: "body-length" header is not an integer: z`},
+ {"body-length: 5", "body-length: 3", "assertion body length and declared body-length don't match: 5 != 3"},
+ {"authority-id: auth-id\n", "", `assertion: "authority-id" header is mandatory`},
+ {"authority-id: auth-id\n", "authority-id: \n", `assertion: "authority-id" header should not be empty`},
+ {keyIDHdr, "", `assertion: "sign-key-sha3-384" header is mandatory`},
+ {keyIDHdr, "sign-key-sha3-384: \n", `assertion: "sign-key-sha3-384" header should not be empty`},
+ {keyIDHdr, "sign-key-sha3-384: $\n", `assertion: "sign-key-sha3-384" header cannot be decoded: .*`},
+ {keyIDHdr, "sign-key-sha3-384: eHl6\n", `assertion: "sign-key-sha3-384" header does not have the expected bit length: 24`},
+ {"AXNpZw==", "", "empty assertion signature"},
+ {"type: test-only\n", "", `assertion: "type" header is mandatory`},
+ {"type: test-only\n", "type: unknown\n", `unknown assertion type: "unknown"`},
+ {"revision: 0\n", "revision: Z\n", `assertion: "revision" header is not an integer: Z`},
+ {"revision: 0\n", "revision:\n - 1\n", `assertion: "revision" header is not an integer: \[1\]`},
+ {"revision: 0\n", "revision: -10\n", "assertion: revision should be positive: -10"},
+ {"format: 0\n", "format: Z\n", `assertion: "format" header is not an integer: Z`},
+ {"format: 0\n", "format: -10\n", "assertion: format should be positive: -10"},
+ {"primary-key: abc\n", "", `assertion test-only: "primary-key" header is mandatory`},
+ {"primary-key: abc\n", "primary-key:\n - abc\n", `assertion test-only: "primary-key" header must be a string`},
+ {"primary-key: abc\n", "primary-key: a/c\n", `assertion test-only: "primary-key" primary key header cannot contain '/'`},
+ {"abcde", "ab\xffde", "body is not utf8"},
+ }
+
+ for _, test := range invalidAssertTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, test.expectedErr)
+ }
+}
+
+func (as *assertsSuite) TestDecodeNoAuthorityInvalid(c *C) {
+ invalid := "type: test-only-no-authority\n" +
+ "authority-id: auth-id1\n" +
+ "hdr: FOO\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "openpgp c2ln"
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, `"test-only-no-authority" assertion cannot have authority-id set`)
+}
+
+func checkContent(c *C, a asserts.Assertion, encoded string) {
+ expected, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ expectedCont, _ := expected.Signature()
+
+ cont, _ := a.Signature()
+ c.Check(cont, DeepEquals, expectedCont)
+}
+
+func (as *assertsSuite) TestEncoderDecoderHappy(c *C) {
+ stream := new(bytes.Buffer)
+ enc := asserts.NewEncoder(stream)
+ asserts.EncoderAppend(enc, []byte(exampleEmptyBody2NlNl))
+ asserts.EncoderAppend(enc, []byte(exampleBodyAndExtraHeaders))
+ asserts.EncoderAppend(enc, []byte(exampleEmptyBodyAllDefaults))
+
+ decoder := asserts.NewDecoder(stream)
+ a, err := decoder.Decode()
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.TestOnlyType)
+ _, ok := a.(*asserts.TestOnly)
+ c.Check(ok, Equals, true)
+ checkContent(c, a, exampleEmptyBody2NlNl)
+
+ a, err = decoder.Decode()
+ c.Assert(err, IsNil)
+ checkContent(c, a, exampleBodyAndExtraHeaders)
+
+ a, err = decoder.Decode()
+ c.Assert(err, IsNil)
+ checkContent(c, a, exampleEmptyBodyAllDefaults)
+
+ a, err = decoder.Decode()
+ c.Assert(err, Equals, io.EOF)
+}
+
+func (as *assertsSuite) TestDecodeEmptyStream(c *C) {
+ stream := new(bytes.Buffer)
+ decoder := asserts.NewDecoder(stream)
+ _, err := decoder.Decode()
+ c.Check(err, Equals, io.EOF)
+}
+
+func (as *assertsSuite) TestDecoderHappyWithSeparatorsVariations(c *C) {
+ streams := []string{
+ exampleBodyAndExtraHeaders,
+ exampleEmptyBody2NlNl,
+ exampleEmptyBodyAllDefaults,
+ }
+
+ for _, streamData := range streams {
+ stream := bytes.NewBufferString(streamData)
+ decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024)
+ a, err := decoder.Decode()
+ c.Assert(err, IsNil, Commentf("stream: %q", streamData))
+
+ checkContent(c, a, streamData)
+
+ a, err = decoder.Decode()
+ c.Check(a, IsNil)
+ c.Check(err, Equals, io.EOF, Commentf("stream: %q", streamData))
+ }
+}
+
+func (as *assertsSuite) TestDecoderHappyWithTrailerDoubleNewlines(c *C) {
+ streams := []string{
+ exampleBodyAndExtraHeaders,
+ exampleEmptyBody2NlNl,
+ exampleEmptyBodyAllDefaults,
+ }
+
+ for _, streamData := range streams {
+ stream := bytes.NewBufferString(streamData)
+ if strings.HasSuffix(streamData, "\n") {
+ stream.WriteString("\n")
+ } else {
+ stream.WriteString("\n\n")
+ }
+
+ decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024)
+ a, err := decoder.Decode()
+ c.Assert(err, IsNil, Commentf("stream: %q", streamData))
+
+ checkContent(c, a, streamData)
+
+ a, err = decoder.Decode()
+ c.Check(a, IsNil)
+ c.Check(err, Equals, io.EOF, Commentf("stream: %q", streamData))
+ }
+}
+
+func (as *assertsSuite) TestDecoderUnexpectedEOF(c *C) {
+ streamData := exampleBodyAndExtraHeaders + "\n" + exampleEmptyBodyAllDefaults
+ fstHeadEnd := strings.Index(exampleBodyAndExtraHeaders, "\n\n")
+ sndHeadEnd := len(exampleBodyAndExtraHeaders) + 1 + strings.Index(exampleEmptyBodyAllDefaults, "\n\n")
+
+ for _, brk := range []int{1, fstHeadEnd / 2, fstHeadEnd, fstHeadEnd + 1, fstHeadEnd + 2, fstHeadEnd + 6} {
+ stream := bytes.NewBufferString(streamData[:brk])
+ decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024)
+ _, err := decoder.Decode()
+ c.Check(err, Equals, io.ErrUnexpectedEOF, Commentf("brk: %d", brk))
+ }
+
+ for _, brk := range []int{sndHeadEnd, sndHeadEnd + 1} {
+ stream := bytes.NewBufferString(streamData[:brk])
+ decoder := asserts.NewDecoder(stream)
+ _, err := decoder.Decode()
+ c.Assert(err, IsNil)
+
+ _, err = decoder.Decode()
+ c.Check(err, Equals, io.ErrUnexpectedEOF, Commentf("brk: %d", brk))
+ }
+}
+
+func (as *assertsSuite) TestDecoderBrokenBodySeparation(c *C) {
+ streamData := strings.Replace(exampleBodyAndExtraHeaders, "THE-BODY\n\n", "THE-BODY", 1)
+ decoder := asserts.NewDecoder(bytes.NewBufferString(streamData))
+ _, err := decoder.Decode()
+ c.Assert(err, ErrorMatches, "missing content/signature separator")
+
+ streamData = strings.Replace(exampleBodyAndExtraHeaders, "THE-BODY\n\n", "THE-BODY\n", 1)
+ decoder = asserts.NewDecoder(bytes.NewBufferString(streamData))
+ _, err = decoder.Decode()
+ c.Assert(err, ErrorMatches, "missing content/signature separator")
+}
+
+func (as *assertsSuite) TestDecoderHeadTooBig(c *C) {
+ decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 4, 4, 1024, 1024)
+ _, err := decoder.Decode()
+ c.Assert(err, ErrorMatches, `error reading assertion headers: maximum size exceeded while looking for delimiter "\\n\\n"`)
+}
+
+func (as *assertsSuite) TestDecoderBodyTooBig(c *C) {
+ decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 1024, 1024, 5, 1024)
+ _, err := decoder.Decode()
+ c.Assert(err, ErrorMatches, "assertion body length 8 exceeds maximum body size")
+}
+
+func (as *assertsSuite) TestDecoderSignatureTooBig(c *C) {
+ decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 4, 1024, 1024, 7)
+ _, err := decoder.Decode()
+ c.Assert(err, ErrorMatches, `error reading assertion signature: maximum size exceeded while looking for delimiter "\\n\\n"`)
+}
+
+func (as *assertsSuite) TestEncode(c *C) {
+ encoded := []byte("type: test-only\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: xyz\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==")
+ a, err := asserts.Decode(encoded)
+ c.Assert(err, IsNil)
+ encodeRes := asserts.Encode(a)
+ c.Check(encodeRes, DeepEquals, encoded)
+}
+
+func (as *assertsSuite) TestEncoderOK(c *C) {
+ encoded := []byte("type: test-only\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: xyzyz\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==")
+ a0, err := asserts.Decode(encoded)
+ c.Assert(err, IsNil)
+ cont0, _ := a0.Signature()
+
+ stream := new(bytes.Buffer)
+ enc := asserts.NewEncoder(stream)
+ enc.Encode(a0)
+
+ c.Check(bytes.HasSuffix(stream.Bytes(), []byte{'\n'}), Equals, true)
+
+ dec := asserts.NewDecoder(stream)
+ a1, err := dec.Decode()
+ c.Assert(err, IsNil)
+
+ cont1, _ := a1.Signature()
+ c.Check(cont1, DeepEquals, cont0)
+}
+
+func (as *assertsSuite) TestEncoderSingleDecodeOK(c *C) {
+ encoded := []byte("type: test-only\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: abc\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==")
+ a0, err := asserts.Decode(encoded)
+ c.Assert(err, IsNil)
+ cont0, _ := a0.Signature()
+
+ stream := new(bytes.Buffer)
+ enc := asserts.NewEncoder(stream)
+ enc.Encode(a0)
+
+ a1, err := asserts.Decode(stream.Bytes())
+ c.Assert(err, IsNil)
+
+ cont1, _ := a1.Signature()
+ c.Check(cont1, DeepEquals, cont0)
+}
+
+func (as *assertsSuite) TestSignFormatSanityEmptyBody(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ }
+ a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ _, err = asserts.Decode(asserts.Encode(a))
+ c.Check(err, IsNil)
+}
+
+func (as *assertsSuite) TestSignFormatSanityNonEmptyBody(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ }
+ body := []byte("THE-BODY")
+ a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, body, testPrivKey1)
+ c.Assert(err, IsNil)
+ c.Check(a.Body(), DeepEquals, body)
+
+ decoded, err := asserts.Decode(asserts.Encode(a))
+ c.Assert(err, IsNil)
+ c.Check(decoded.Body(), DeepEquals, body)
+}
+
+func (as *assertsSuite) TestSignFormatSanitySupportMultilineHeaderValues(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ }
+
+ multilineVals := []string{
+ "a\n",
+ "\na",
+ "a\n\b\nc",
+ "a\n\b\nc\n",
+ "\na\n",
+ "\n\na\n\nb\n\nc",
+ }
+
+ for _, multilineVal := range multilineVals {
+ headers["multiline"] = multilineVal
+ if len(multilineVal)%2 == 1 {
+ headers["odd"] = "true"
+ }
+
+ a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ decoded, err := asserts.Decode(asserts.Encode(a))
+ c.Assert(err, IsNil)
+
+ c.Check(decoded.Header("multiline"), Equals, multilineVal)
+ }
+}
+
+func (as *assertsSuite) TestSignFormatAndRevision(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ "format": "1",
+ "revision": "11",
+ }
+
+ a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ c.Check(a.Revision(), Equals, 11)
+ c.Check(a.Format(), Equals, 1)
+ c.Check(a.SupportedFormat(), Equals, true)
+
+ a1, err := asserts.Decode(asserts.Encode(a))
+ c.Assert(err, IsNil)
+
+ c.Check(a1.Revision(), Equals, 11)
+ c.Check(a1.Format(), Equals, 1)
+ c.Check(a1.SupportedFormat(), Equals, true)
+}
+
+func (as *assertsSuite) TestSignBodyIsUTF8Text(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ }
+ _, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, []byte{'\xff'}, testPrivKey1)
+ c.Assert(err, ErrorMatches, "assertion body is not utf8")
+}
+
+func (as *assertsSuite) TestHeaders(c *C) {
+ encoded := []byte("type: test-only\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: abc\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==")
+ a, err := asserts.Decode(encoded)
+ c.Assert(err, IsNil)
+
+ hs := a.Headers()
+ c.Check(hs, DeepEquals, map[string]interface{}{
+ "type": "test-only",
+ "authority-id": "auth-id2",
+ "primary-key": "abc",
+ "revision": "5",
+ "header1": "value1",
+ "header2": "value2",
+ "body-length": "8",
+ "sign-key-sha3-384": exKeyID,
+ })
+}
+
+func (as *assertsSuite) TestHeadersReturnsCopy(c *C) {
+ encoded := []byte("type: test-only\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: xyz\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==")
+ a, err := asserts.Decode(encoded)
+ c.Assert(err, IsNil)
+
+ hs := a.Headers()
+ // casual later result mutation doesn't trip us
+ delete(hs, "primary-key")
+ c.Check(a.Header("primary-key"), Equals, "xyz")
+}
+
+func (as *assertsSuite) TestAssembleRoundtrip(c *C) {
+ encoded := []byte("type: test-only\n" +
+ "format: 1\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: abc\n" +
+ "revision: 5\n" +
+ "header1: value1\n" +
+ "header2: value2\n" +
+ "body-length: 8\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "THE-BODY" +
+ "\n\n" +
+ "AXNpZw==")
+ a, err := asserts.Decode(encoded)
+ c.Assert(err, IsNil)
+
+ cont, sig := a.Signature()
+ reassembled, err := asserts.Assemble(a.Headers(), a.Body(), cont, sig)
+ c.Assert(err, IsNil)
+
+ c.Check(reassembled.Headers(), DeepEquals, a.Headers())
+ c.Check(reassembled.Body(), DeepEquals, a.Body())
+
+ reassembledEncoded := asserts.Encode(reassembled)
+ c.Check(reassembledEncoded, DeepEquals, encoded)
+}
+
+func (as *assertsSuite) TestSignKeyID(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ }
+ a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ keyID := a.SignKeyID()
+ c.Check(keyID, Equals, testPrivKey1.PublicKey().ID())
+}
+
+func (as *assertsSuite) TestSelfRef(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "auth-id1",
+ "primary-key": "0",
+ }
+ a1, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ c.Check(a1.Ref(), DeepEquals, &asserts.Ref{
+ Type: asserts.TestOnlyType,
+ PrimaryKey: []string{"0"},
+ })
+
+ headers = map[string]interface{}{
+ "authority-id": "auth-id1",
+ "pk1": "a",
+ "pk2": "b",
+ }
+ a2, err := asserts.AssembleAndSignInTest(asserts.TestOnly2Type, headers, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ c.Check(a2.Ref(), DeepEquals, &asserts.Ref{
+ Type: asserts.TestOnly2Type,
+ PrimaryKey: []string{"a", "b"},
+ })
+}
+
+func (as *assertsSuite) TestAssembleHeadersCheck(c *C) {
+ cont := []byte("type: test-only\n" +
+ "authority-id: auth-id2\n" +
+ "primary-key: abc\n" +
+ "revision: 5")
+ headers := map[string]interface{}{
+ "type": "test-only",
+ "authority-id": "auth-id2",
+ "primary-key": "abc",
+ "revision": 5, // must be a string actually!
+ }
+
+ _, err := asserts.Assemble(headers, nil, cont, nil)
+ c.Check(err, ErrorMatches, `header "revision": header values must be strings or nested lists or maps with strings as the only scalars: 5`)
+}
+
+func (as *assertsSuite) TestSignWithoutAuthorityMisuse(c *C) {
+ _, err := asserts.SignWithoutAuthority(asserts.TestOnlyType, nil, nil, testPrivKey1)
+ c.Check(err, ErrorMatches, `cannot sign assertions needing a definite authority with SignWithoutAuthority`)
+
+ _, err = asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType,
+ map[string]interface{}{
+ "authority-id": "auth-id1",
+ "hdr": "FOO",
+ }, nil, testPrivKey1)
+ c.Check(err, ErrorMatches, `"test-only-no-authority" assertion cannot have authority-id set`)
+}
+
+func (ss *serialSuite) TestSignatureCheckError(c *C) {
+ sreq, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType,
+ map[string]interface{}{
+ "hdr": "FOO",
+ }, nil, testPrivKey1)
+ c.Assert(err, IsNil)
+
+ err = asserts.SignatureCheck(sreq, testPrivKey2.PublicKey())
+ c.Check(err, ErrorMatches, `failed signature verification:.*`)
+}
+
+func (as *assertsSuite) TestWithAuthority(c *C) {
+ withAuthority := []string{
+ "account",
+ "account-key",
+ "base-declaration",
+ "snap-declaration",
+ "snap-build",
+ "snap-revision",
+ "model",
+ "serial",
+ "system-user",
+ "validation",
+ }
+ c.Check(withAuthority, HasLen, asserts.NumAssertionType-3) // excluding device-session-request, serial-request, account-key-request
+ for _, name := range withAuthority {
+ typ := asserts.Type(name)
+ _, err := asserts.AssembleAndSignInTest(typ, nil, nil, testPrivKey1)
+ c.Check(err, ErrorMatches, `"authority-id" header is mandatory`)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package assertstest provides helpers for testing code that involves assertions.
+package assertstest
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "os/exec"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/openpgp/armor"
+ "golang.org/x/crypto/openpgp/packet"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/strutil"
+)
+
+// GenerateKey generates a private/public key pair of the given bits. It panics on error.
+func GenerateKey(bits int) (asserts.PrivateKey, *rsa.PrivateKey) {
+ priv, err := rsa.GenerateKey(rand.Reader, bits)
+ if err != nil {
+ panic(fmt.Errorf("failed to create private key: %v", err))
+ }
+ return asserts.RSAPrivateKey(priv), priv
+}
+
+// ReadPrivKey reads a PGP private key (either armored or simply base64 encoded). It panics on error.
+func ReadPrivKey(pk string) (asserts.PrivateKey, *rsa.PrivateKey) {
+ rd := bytes.NewReader([]byte(pk))
+ blk, err := armor.Decode(rd)
+ var body io.Reader
+ if err == nil {
+ body = blk.Body
+ } else {
+ rd.Seek(0, 0)
+ // try unarmored
+ body = base64.NewDecoder(base64.StdEncoding, rd)
+ }
+ pkt, err := packet.Read(body)
+ if err != nil {
+ panic(err)
+ }
+
+ pkPkt := pkt.(*packet.PrivateKey)
+ rsaPrivKey, ok := pkPkt.PrivateKey.(*rsa.PrivateKey)
+ if !ok {
+ panic("not a RSA key")
+ }
+
+ return asserts.RSAPrivateKey(rsaPrivKey), rsaPrivKey
+}
+
+// A sample developer key.
+// See systestkeys for a prebuilt set of trusted keys and assertions.
+const (
+ DevKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1
+
+lQcYBFaFwYABEAC0kYiC4rsWFLJHEv/qO93LTMCAYKMLXFU0XN4XvqnkbwFc0QQd
+lQcr7PwavYmKdWum+EmGWV/k5vZ0gwfZhBsL2MTWSNvO+5q5AYOqTq01CbSLcoN4
+cJI+BU348Vc/AoiIuuHro+gALs59HWsVSAKq7SNyHQfo257TKe8Q+Jjh095eruYJ
+2kOvlAgAzjUv7eGDQ53O87wcwgZlCl0XqM/t+SRUxE5i8dQ4nySSekoTsWJo02kf
+uMrWo3E5iEt6KKhfQtit2ZO91NYetIplzzZmaUOOkpziFTFW1NcwDKzDsLMh1EQ+
+ib+8mSWcou9m35aTkAQXlXlgqe5Pelj5+NUxnnoa1MR478Sv+guT+fbFQrl8PkMD
+Jb/3PTKDPBNtjki5ZfIN9id4vidfBY4SCDftnj7yZMf5+1PPZ2XXHUoiUhHbGjST
+F/23wr6OWvXe/AXX5BF4wJJTJxSxnYR6nleGMj4sbsbVsxIaqh1lMg5cuQjLr7eI
+nxn994geUnQQsEPIVuVjLThJ/0sjXjy8kyxh6eieShZ6NZ0yLyIJRN5pnJ0ckRQF
+T9Fs0UuMJZro0hR71t9mAuI45mSmznj78AvTvyuL+0aOj/lQa97NKbCsShYnKqsm
+3Yzr03ahUMslwd6jZtRg+0ULYp9vaN7nwmsn6WWJ92CsCzFucdeJfJWKZQARAQAB
+AA/9GSda3mzKRhms+hSt/MnJLFxtRpTvsZHztp8nOySO0ykZhf4B9kL/5EEXn3v+
+0IBp9jEJQQNrRd5cv79PFSB/igdw6C7vG+bV12bcGhnqrARFl9Vkdh8saCJiCcdI
+8ZifP3jVJvfGxlu+3RP/ik/lOz1cnjVoGCqb9euWB4Wx+meCxyrTFdVHb4qOENqo
+8xvOufPt5Fn0vwbSUDoA3N5h1NNLmdlc2BC7EQYuWI9biWHBBTxKHSanbv4GtE6F
+wScvyVFtEM7J83xWNaHN07/pYpvQUuienSn5nRB6R5HEcWBIm/JPbWzP/mxRHoBe
+HDUSa0z5HPXwGiSh84VmJrBgtlQosxk3jOHjynlU194S2cVLcSrFSf4hp6WZVAa1
+Nlkv6v62eU3nDxabkF92Lcv40s1cBqYCvhOtMzgoXL0TuaVJIdUwbJRHoBi8Bh5f
+bNYJqyMqJNHcT9ylAWw130ljPTtqzbTMRtitxnJPbf60hpsJ4jcp2bJP9pg9XyuR
+ZyIKtLfGQfxvFLsXzNssnVv7ZenK5AgUFTMvmyKCQQeYluheKc0KtRKSYE3iaVAs
+Efw5Pd0GD82UGef9WahtnemodTlD3nkzlD50XBsd8xdNBQ7N2TFsP5Ldvfp1Wf2F
+qg+rTaS0OID9vDQuekOcDI8lA9E4FYlIkJ6AqIb7hD5hlBMIAMRVXLlPLgzmrY5k
+pIPMbgyN0wm3f4qAWIaMtg79x9gTylsGF7lkqNLqFDFYfoUHb+iXINYc51kHV7Ka
+JifHhdy8TaBTBrIrsFLJpv06lRex/fdyvswev3W1g3wRJ86eTCqwr1DjB+q2kYX8
+u1qDPFRzK4WF+wOF/FwCBLDpESmHSapXuzL5i6pJfOCFIJqT/Q/yp9tyTcxs82tu
+kSlNKoXrZi4xHsDpPBuNjMl3eIz3ogesUG60MMa6xovVGV3ICJcwYwycvvQcjuxS
+XtJlHK+/G3kB87BXzNCMyUGfDNy7mcTrXAXoUH8nCu4ipyaT/jEyvi95w/7RJcFU
+qs6taH8IAOtxqnBZGDQuYPF0ZmZQ7e1/FXq/LBQryYZgNtyLUdR7ycXGCTXlEIGw
+X3r7Qf4+a3MlriP5thKxci+npcIj4e31aS6cpO2IcGJzmNOHzLCl3b4XmO/APBSA
+FZpQE3k+lg45tn/vgcPMKKLAAv6TbpVVgLrFXGtX3Gtkd66fPPOcINXi6+MqXfp5
+rl8OJIq5O5ygbbglwcqmeI29RLZ58b0ktCa5ZZNzeSV+T5jHwRNnWm0EJgjx8Lwn
+LEWFS/vQjGwaoRJi06jpmM+66sefyTQ3qvyzQLBqlenZf16GGz28cOSjNJ9FDth1
+iKnyk7d8nqhmbSHeW08QUwTF6NGp+xsIAJDa3ouxSjTEB1P7z1VLJp6nSglBQ74n
+XAprk2WpggLNrWwyFJsgFh07LxShb/O3t1TanU+Ld/ryyWHztTxag2auAHuVQ4+S
+EkjKqkUaSOQML9a9AvZ2rQr84f5ohc/vCOQhpNVLSyw55EW6WhnntNWVwgZxMiAj
+oREMJMrBb6LL9b7kHtfYqLNfe3fkUx+tuTsm96Wi1cdkh0qyut0+J+eieZVn7kiM
+UP5IZuz9TSjDOrA5qu5NGlbXNaN0cdJ2UUSNekQiysqDpdf00wIwr1XqH+KLUjZv
+pO5Mub6NdnVXJRZunpbNXbuxj49NXnZEEi71WQm9KLR8KQ1oQ+RlnHx/XLQHICh0
+ZXN0KYkCOAQTAQIAIgUCVoXBgAIbLwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA
+CgkQSkI9KKrqS0/YEhAAgJALHrx4kFRcgDJE+khK/CdoaLvi0N40eAE+RzQgcxhh
+S4Aeks8n1cL6oAwDfCL+ohyWvPzF2DzsBkEIC3l+JS2tn0JJ+qexY+qhdGkEze/o
+SIvH9sfR5LJuKb3OAt2mQlY+sxjlkzU9rTGKsVZxgApNM4665dlagF9tipMQTHnd
+eFZRlvNTWKkweW0jbJCpRKlQnjEZ6S/wlPBgH69Ek3bnDcgp6eaAU92Ke9Fa2wMV
+LBMaXpUIvddKFjoGtvShDOpcQRE99Z8tK4YSAOg+zbSUeD7HGH00EQERItoJsAv1
+7Du8+jcKSeOhz7PPxOA7mEnYNdoMcrg/2AP+FVI6zGYcKN7Hq3C6Z+bQ4X1VkKmv
+NCFomU2AyPVxpJRYw7/EkoRWp/iq6sEb7bsmhmDEiz1MiroAV+efmWyUjxueSzrW
+24OxHTWi2GuHBF+FKUD3UxfaWMjH+tuWYPIHzYsT+TfsN0vAEFyhRi8Ncelu1RV4
+x2O3wmjxoaX/2FmyuU5WhcVkcpRFgceyf1/86NP9gT5MKbWtJC85YYpxibnvPdGd
++sqtEEqgX3dSsHT+rkBk7kf3ghDwsLtnliFPOeAaIHGZl754EpK+qPUTnYZK022H
+2crhYlApO9+06kBeybSO6joMUR007883I9GELYhzmuEjpVGquJQ3+S5QtW1to0w=
+=5Myf
+-----END PGP PRIVATE KEY BLOCK-----
+`
+
+ DevKeyID = "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu"
+
+ DevKeyPGPFingerprint = "966e70f4b9f257a2772f8f354a423d28aaea4b4f"
+)
+
+// GPGImportKey imports the given PGP armored key into the GnuPG setup at homedir. It panics on error.
+func GPGImportKey(homedir, armoredKey string) {
+ path, err := exec.LookPath("gpg1")
+ if err != nil {
+ path, err = exec.LookPath("gpg")
+ }
+ if err != nil {
+ panic(err)
+ }
+ gpg := exec.Command(path, "--homedir", homedir, "-q", "--batch", "--import", "--armor")
+ gpg.Stdin = bytes.NewBufferString(armoredKey)
+ out, err := gpg.CombinedOutput()
+ if err != nil {
+ panic(fmt.Errorf("cannot import test key into GPG setup at %q: %v (%q)", homedir, err, out))
+ }
+}
+
+// A SignerDB can sign assertions using its key pairs.
+type SignerDB interface {
+ Sign(assertType *asserts.AssertionType, headers map[string]interface{}, body []byte, keyID string) (asserts.Assertion, error)
+}
+
+// NewAccount creates an account assertion for username, it fills in values for other missing headers as needed. It panics on error.
+func NewAccount(db SignerDB, username string, otherHeaders map[string]interface{}, keyID string) *asserts.Account {
+ if otherHeaders == nil {
+ otherHeaders = make(map[string]interface{})
+ }
+ otherHeaders["username"] = username
+ if otherHeaders["account-id"] == nil {
+ otherHeaders["account-id"] = strutil.MakeRandomString(32)
+ }
+ if otherHeaders["display-name"] == nil {
+ otherHeaders["display-name"] = strings.ToTitle(username[:1]) + username[1:]
+ }
+ if otherHeaders["validation"] == nil {
+ otherHeaders["validation"] = "unproven"
+ }
+ if otherHeaders["timestamp"] == nil {
+ otherHeaders["timestamp"] = time.Now().Format(time.RFC3339)
+ }
+ a, err := db.Sign(asserts.AccountType, otherHeaders, nil, keyID)
+ if err != nil {
+ panic(err)
+ }
+ return a.(*asserts.Account)
+}
+
+// NewAccountKey creates an account-key assertion for the account, it fills in values for missing headers as needed. In panics on error.
+func NewAccountKey(db SignerDB, acct *asserts.Account, otherHeaders map[string]interface{}, pubKey asserts.PublicKey, keyID string) *asserts.AccountKey {
+ if otherHeaders == nil {
+ otherHeaders = make(map[string]interface{})
+ }
+ otherHeaders["account-id"] = acct.AccountID()
+ otherHeaders["public-key-sha3-384"] = pubKey.ID()
+ if otherHeaders["name"] == nil {
+ otherHeaders["name"] = "default"
+ }
+ if otherHeaders["since"] == nil {
+ otherHeaders["since"] = time.Now().Format(time.RFC3339)
+ }
+ encodedPubKey, err := asserts.EncodePublicKey(pubKey)
+ if err != nil {
+ panic(err)
+ }
+ a, err := db.Sign(asserts.AccountKeyType, otherHeaders, encodedPubKey, keyID)
+ if err != nil {
+ panic(err)
+ }
+ return a.(*asserts.AccountKey)
+}
+
+// SigningDB embeds a signing assertion database with a default private key and assigned authority id.
+// Sign will use the assigned authority id.
+// "" can be passed for keyID to Sign and PublicKey to use the default key.
+type SigningDB struct {
+ AuthorityID string
+ KeyID string
+
+ *asserts.Database
+}
+
+// NewSigningDB creates a test signing assertion db with the given defaults. It panics on error.
+func NewSigningDB(authorityID string, privKey asserts.PrivateKey) *SigningDB {
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{})
+ if err != nil {
+ panic(err)
+ }
+ err = db.ImportKey(privKey)
+ if err != nil {
+ panic(err)
+ }
+ return &SigningDB{
+ AuthorityID: authorityID,
+ KeyID: privKey.PublicKey().ID(),
+ Database: db,
+ }
+}
+
+func (db *SigningDB) Sign(assertType *asserts.AssertionType, headers map[string]interface{}, body []byte, keyID string) (asserts.Assertion, error) {
+ headers["authority-id"] = db.AuthorityID
+ if keyID == "" {
+ keyID = db.KeyID
+ }
+ return db.Database.Sign(assertType, headers, body, keyID)
+}
+
+func (db *SigningDB) PublicKey(keyID string) (asserts.PublicKey, error) {
+ if keyID == "" {
+ keyID = db.KeyID
+ }
+ return db.Database.PublicKey(keyID)
+}
+
+// StoreStack realises a store-like set of founding trusted assertions and signing setup.
+type StoreStack struct {
+ // Trusted authority assertions.
+ TrustedAccount *asserts.Account
+ TrustedKey *asserts.AccountKey
+ Trusted []asserts.Assertion
+
+ // Signing assertion db that signs with the root private key.
+ RootSigning *SigningDB
+
+ // The store-like signing functionality that signs with a store key, setup to also store assertions if desired. It stores a default account-key for the store private key, see also the StoreStack.Key method.
+ *SigningDB
+}
+
+// NewStoreStack creates a new store assertion stack. It panics on error.
+func NewStoreStack(authorityID string, rootPrivKey, storePrivKey asserts.PrivateKey) *StoreStack {
+ rootSigning := NewSigningDB(authorityID, rootPrivKey)
+ ts := time.Now().Format(time.RFC3339)
+ trustedAcct := NewAccount(rootSigning, authorityID, map[string]interface{}{
+ "account-id": authorityID,
+ "validation": "certified",
+ "timestamp": ts,
+ }, "")
+ trustedKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{
+ "name": "root",
+ "since": ts,
+ }, rootPrivKey.PublicKey(), "")
+ trusted := []asserts.Assertion{trustedAcct, trustedKey}
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: trusted,
+ })
+ if err != nil {
+ panic(err)
+ }
+ err = db.ImportKey(storePrivKey)
+ if err != nil {
+ panic(err)
+ }
+ storeKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{
+ "name": "store",
+ }, storePrivKey.PublicKey(), "")
+ err = db.Add(storeKey)
+ if err != nil {
+ panic(err)
+ }
+
+ return &StoreStack{
+ TrustedAccount: trustedAcct,
+ TrustedKey: trustedKey,
+ Trusted: trusted,
+
+ RootSigning: rootSigning,
+
+ SigningDB: &SigningDB{
+ AuthorityID: authorityID,
+ KeyID: storeKey.PublicKeyID(),
+ Database: db,
+ },
+ }
+}
+
+// StoreAccountKey retrieves one of the account-key assertions for the signing keys of the simulated store signing database.
+// "" for keyID means the default one. It panics on error.
+func (ss *StoreStack) StoreAccountKey(keyID string) *asserts.AccountKey {
+ if keyID == "" {
+ keyID = ss.KeyID
+ }
+ key, err := ss.Find(asserts.AccountKeyType, map[string]string{
+ "account-id": ss.AuthorityID,
+ "public-key-sha3-384": keyID,
+ })
+ if err == asserts.ErrNotFound {
+ return nil
+ }
+ if err != nil {
+ panic(err)
+ }
+ return key.(*asserts.AccountKey)
+}
+
+// MockBuiltinBaseDeclaration mocks the builtin base-declaration exposed by asserts.BuiltinBaseDeclaration.
+func MockBuiltinBaseDeclaration(headers []byte) (restore func()) {
+ var prevHeaders []byte
+ decl := asserts.BuiltinBaseDeclaration()
+ if decl != nil {
+ prevHeaders, _ = decl.Signature()
+ }
+
+ err := asserts.InitBuiltinBaseDeclaration(headers)
+ if err != nil {
+ panic(err)
+ }
+
+ return func() {
+ err := asserts.InitBuiltinBaseDeclaration(prevHeaders)
+ if err != nil {
+ panic(err)
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package assertstest_test
+
+import (
+ "encoding/hex"
+ "testing"
+ "time"
+
+ "golang.org/x/crypto/openpgp/packet"
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+func TestAssertsTest(t *testing.T) { TestingT(t) }
+
+type helperSuite struct{}
+
+var _ = Suite(&helperSuite{})
+
+func (s *helperSuite) TestReadPrivKeyArmored(c *C) {
+ pk, rsaPrivKey := assertstest.ReadPrivKey(assertstest.DevKey)
+ c.Check(pk, NotNil)
+ c.Check(rsaPrivKey, NotNil)
+ c.Check(pk.PublicKey().ID(), Equals, assertstest.DevKeyID)
+ pkt := packet.NewRSAPrivateKey(time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), rsaPrivKey)
+ c.Check(hex.EncodeToString(pkt.Fingerprint[:]), Equals, assertstest.DevKeyPGPFingerprint)
+}
+
+const (
+ base64PrivKey = `
+xcLYBFaU5cgBCAC/2wUYK7YzvL6f0ZxBfptFVfNmI7G9J9Eszdoq1NZZXaV+aYeC7eNU
+1sKdO6wIRcw3lvybtq5W1n4D/jJAb2qXbB6BukuCGVXCLMEUdvheaVVcIZ/LwdbxmgMJsDFoHsDC
+RzjkUVTU2b8sK6MwANIsSS5r8Lwm7FazD1qq50UdebsIx8dkjFR5VwrCYgOu1MO2Bqka7UU9as2q
+4ZsFzpcS/so41kd4IPFEmNMlejhSjgCaixehpLeXypQVHLluV+oSPMV7GtE7Z6HO4V5cT2c9RdXg
+l4jSKY91rHInkmSizF03laL3T/I6oj0FdZG9GB6QzqRCBTzK05cnVP1k7WFJABEBAAEAB/9spiIa
+cBa88fSaGWB+Dq7r8yLmAuzTDEt/LgyRGPtSnJ/uGOEvGn0VPJH17ScdgDmIea8Ql8HfV5UBueDH
+cNFSc15LZS8BvEs+rY2ig0VgYhJ/HGOcRmftZqS1xdwU9OWAoEjts8lwyOdkoknGE5Dyl3b8ldZX
+zJvEx7s28cXITH4UwGEAMHEXrAMCjkcKPVbM7vW81uOWn0U1jMzmfmqrcLkSfvaCnep6+4QphKPy
+B4DxJAI34EvJAru4iL5bWWvMeXkBZgmBy4g2SlYbk09cfTmhzw6di5GZtg+77yGACltPBA8MSbzF
+v30apQ5iuI/hVin7U2/QtQHP4d0zUDbpBADusynnaFcDnPEUm4RdvNpujaBC/HfIpOstiS36RZy8
+lZeVtffa/+DqzodZD9YF7zEVWeUiC5Os4THirYOZ04dM5yqR/GlKXMHGHaT+mnhD8g1hORx/LrMO
+k5wUpD1NmloSjP/0pJRccuXq7O1QQfls1Hq1vOSh3cZ/aIvTONJ/YwQAzcK0/2SrnaUc3oCxMEuI
+2FX0LsYDQiXzMK/x/lfZ/ywxt5J/q6CuaG3xXgSHlsk0M8Uo4acZqpCIFA9mwCPxKbrIOGnwJsI/
++sZBkngtZMSS88Vl32gnzpVWLGpbW2F7hnWrj1YigTcFUdi6TFNa7zHPASzCKxKKiz9YxEWWymME
+AIbURnQJJOSfYgFyloQuA2QWyAK5Zu7qPworBoRo+PZPVb5yQmSUQ21VqNfzqIJz1EgiDZ0NyGid
+uXAjn58O9tAq7IN5pTeHoTacZ75cI82kQkUxEnfiKjBO/AU30Y3COsIXhtbIXbtcitHSicp4lnpU
+NejDkxUnC2wIvJzHWo1FQ18=
+`
+)
+
+func (s *helperSuite) TestReadPrivKeyUnarmored(c *C) {
+ pk, rsaPrivKey := assertstest.ReadPrivKey(base64PrivKey)
+ c.Check(pk, NotNil)
+ c.Check(rsaPrivKey, NotNil)
+}
+
+func (s *helperSuite) TestStoreStack(c *C) {
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+
+ store := assertstest.NewStoreStack("super", rootPrivKey, storePrivKey)
+
+ c.Check(store.TrustedAccount.AccountID(), Equals, "super")
+ c.Check(store.TrustedAccount.IsCertified(), Equals, true)
+
+ c.Check(store.TrustedKey.AccountID(), Equals, "super")
+ c.Check(store.TrustedKey.Name(), Equals, "root")
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: store.Trusted,
+ })
+ c.Assert(err, IsNil)
+
+ storeAccKey := store.StoreAccountKey("")
+ c.Assert(storeAccKey, NotNil)
+
+ c.Check(storeAccKey.AccountID(), Equals, "super")
+ c.Check(storeAccKey.AccountID(), Equals, store.AuthorityID)
+ c.Check(storeAccKey.PublicKeyID(), Equals, store.KeyID)
+ c.Check(storeAccKey.Name(), Equals, "store")
+
+ acct := assertstest.NewAccount(store, "devel1", nil, "")
+ c.Check(acct.Username(), Equals, "devel1")
+ c.Check(acct.AccountID(), HasLen, 32)
+ c.Check(acct.IsCertified(), Equals, false)
+
+ err = db.Add(storeAccKey)
+ c.Assert(err, IsNil)
+
+ err = db.Add(acct)
+ c.Assert(err, IsNil)
+
+ devKey, _ := assertstest.GenerateKey(752)
+
+ acctKey := assertstest.NewAccountKey(store, acct, nil, devKey.PublicKey(), "")
+
+ err = db.Add(acctKey)
+ c.Assert(err, IsNil)
+
+ c.Check(acctKey.Name(), Equals, "default")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ _ "crypto/sha256" // be explicit about supporting SHA256
+ _ "crypto/sha512" // be explicit about needing SHA512
+ "encoding/base64"
+ "fmt"
+ "io"
+ "time"
+
+ "golang.org/x/crypto/openpgp/packet"
+ "golang.org/x/crypto/sha3"
+)
+
+const (
+ maxEncodeLineLength = 76
+ v1 = 0x1
+)
+
+var (
+ v1Header = []byte{v1}
+ v1FixedTimestamp = time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)
+)
+
+func encodeV1(data []byte) []byte {
+ buf := new(bytes.Buffer)
+ buf.Grow(base64.StdEncoding.EncodedLen(len(data) + 1))
+ enc := base64.NewEncoder(base64.StdEncoding, buf)
+ enc.Write(v1Header)
+ enc.Write(data)
+ enc.Close()
+ flat := buf.Bytes()
+ flatSize := len(flat)
+
+ buf = new(bytes.Buffer)
+ buf.Grow(flatSize + flatSize/maxEncodeLineLength + 1)
+ off := 0
+ for {
+ endOff := off + maxEncodeLineLength
+ if endOff > flatSize {
+ endOff = flatSize
+ }
+ buf.Write(flat[off:endOff])
+ off = endOff
+ if off >= flatSize {
+ break
+ }
+ buf.WriteByte('\n')
+ }
+
+ return buf.Bytes()
+}
+
+type keyEncoder interface {
+ keyEncode(w io.Writer) error
+}
+
+func encodeKey(key keyEncoder, kind string) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ err := key.keyEncode(buf)
+ if err != nil {
+ return nil, fmt.Errorf("cannot encode %s: %v", kind, err)
+ }
+ return encodeV1(buf.Bytes()), nil
+}
+
+type openpgpSigner interface {
+ sign(content []byte) (*packet.Signature, error)
+}
+
+func signContent(content []byte, privateKey PrivateKey) ([]byte, error) {
+ signer, ok := privateKey.(openpgpSigner)
+ if !ok {
+ panic(fmt.Errorf("not an internally supported PrivateKey: %T", privateKey))
+ }
+
+ sig, err := signer.sign(content)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := new(bytes.Buffer)
+ err = sig.Serialize(buf)
+ if err != nil {
+ return nil, err
+ }
+
+ return encodeV1(buf.Bytes()), nil
+}
+
+func decodeV1(b []byte, kind string) (packet.Packet, error) {
+ if len(b) == 0 {
+ return nil, fmt.Errorf("cannot decode %s: no data", kind)
+ }
+ buf := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
+ n, err := base64.StdEncoding.Decode(buf, b)
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode %s: %v", kind, err)
+ }
+ if n == 0 {
+ return nil, fmt.Errorf("cannot decode %s: base64 without data", kind)
+ }
+ buf = buf[:n]
+ if buf[0] != v1 {
+ return nil, fmt.Errorf("unsupported %s format version: %d", kind, buf[0])
+ }
+ rd := bytes.NewReader(buf[1:])
+ pkt, err := packet.Read(rd)
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode %s: %v", kind, err)
+ }
+ if rd.Len() != 0 {
+ return nil, fmt.Errorf("%s has spurious trailing data", kind)
+ }
+ return pkt, nil
+}
+
+func decodeSignature(signature []byte) (*packet.Signature, error) {
+ pkt, err := decodeV1(signature, "signature")
+ if err != nil {
+ return nil, err
+ }
+ sig, ok := pkt.(*packet.Signature)
+ if !ok {
+ return nil, fmt.Errorf("expected signature, got instead: %T", pkt)
+ }
+ return sig, nil
+}
+
+// PublicKey is the public part of a cryptographic private/public key pair.
+type PublicKey interface {
+ // ID returns the id of the key used for lookup.
+ ID() string
+
+ // verify verifies signature is valid for content using the key.
+ verify(content []byte, sig *packet.Signature) error
+
+ keyEncoder
+}
+
+type openpgpPubKey struct {
+ pubKey *packet.PublicKey
+ sha3_384 string
+}
+
+func (opgPubKey *openpgpPubKey) ID() string {
+ return opgPubKey.sha3_384
+}
+
+func (opgPubKey *openpgpPubKey) verify(content []byte, sig *packet.Signature) error {
+ h := sig.Hash.New()
+ h.Write(content)
+ return opgPubKey.pubKey.VerifySignature(h, sig)
+}
+
+func (opgPubKey openpgpPubKey) keyEncode(w io.Writer) error {
+ return opgPubKey.pubKey.Serialize(w)
+}
+
+func newOpenPGPPubKey(intPubKey *packet.PublicKey) *openpgpPubKey {
+ h := sha3.New384()
+ h.Write(v1Header)
+ err := intPubKey.Serialize(h)
+ if err != nil {
+ panic("internal error: cannot compute public key sha3-384")
+ }
+ sha3_384, err := EncodeDigest(crypto.SHA3_384, h.Sum(nil))
+ if err != nil {
+ panic("internal error: cannot compute public key sha3-384")
+ }
+ return &openpgpPubKey{pubKey: intPubKey, sha3_384: sha3_384}
+}
+
+// RSAPublicKey returns a database useable public key out of rsa.PublicKey.
+func RSAPublicKey(pubKey *rsa.PublicKey) PublicKey {
+ intPubKey := packet.NewRSAPublicKey(v1FixedTimestamp, pubKey)
+ return newOpenPGPPubKey(intPubKey)
+}
+
+// DecodePublicKey deserializes a public key.
+func DecodePublicKey(pubKey []byte) (PublicKey, error) {
+ pkt, err := decodeV1(pubKey, "public key")
+ if err != nil {
+ return nil, err
+ }
+ pubk, ok := pkt.(*packet.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("expected public key, got instead: %T", pkt)
+ }
+ rsaPubKey, ok := pubk.PublicKey.(*rsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("expected RSA public key, got instead: %T", pubk.PublicKey)
+ }
+ return RSAPublicKey(rsaPubKey), nil
+}
+
+// EncodePublicKey serializes a public key, typically for embedding in an assertion.
+func EncodePublicKey(pubKey PublicKey) ([]byte, error) {
+ return encodeKey(pubKey, "public key")
+}
+
+// PrivateKey is a cryptographic private/public key pair.
+type PrivateKey interface {
+ // PublicKey returns the public part of the pair.
+ PublicKey() PublicKey
+
+ keyEncoder
+}
+
+type openpgpPrivateKey struct {
+ privk *packet.PrivateKey
+}
+
+func (opgPrivK openpgpPrivateKey) PublicKey() PublicKey {
+ return newOpenPGPPubKey(&opgPrivK.privk.PublicKey)
+}
+
+func (opgPrivK openpgpPrivateKey) keyEncode(w io.Writer) error {
+ return opgPrivK.privk.Serialize(w)
+}
+
+var openpgpConfig = &packet.Config{
+ DefaultHash: crypto.SHA512,
+}
+
+func (opgPrivK openpgpPrivateKey) sign(content []byte) (*packet.Signature, error) {
+ privk := opgPrivK.privk
+ sig := new(packet.Signature)
+ sig.PubKeyAlgo = privk.PubKeyAlgo
+ sig.Hash = openpgpConfig.Hash()
+ sig.CreationTime = time.Now()
+
+ h := openpgpConfig.Hash().New()
+ h.Write(content)
+
+ err := sig.Sign(h, privk, openpgpConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return sig, nil
+}
+
+func decodePrivateKey(privKey []byte) (PrivateKey, error) {
+ pkt, err := decodeV1(privKey, "private key")
+ if err != nil {
+ return nil, err
+ }
+ privk, ok := pkt.(*packet.PrivateKey)
+ if !ok {
+ return nil, fmt.Errorf("expected private key, got instead: %T", pkt)
+ }
+ if _, ok := privk.PrivateKey.(*rsa.PrivateKey); !ok {
+ return nil, fmt.Errorf("expected RSA private key, got instead: %T", privk.PrivateKey)
+ }
+ return openpgpPrivateKey{privk}, nil
+}
+
+// RSAPrivateKey returns a PrivateKey for database use out of a rsa.PrivateKey.
+func RSAPrivateKey(privk *rsa.PrivateKey) PrivateKey {
+ intPrivk := packet.NewRSAPrivateKey(v1FixedTimestamp, privk)
+ return openpgpPrivateKey{intPrivk}
+}
+
+// GenerateKey generates a private/public key pair.
+func GenerateKey() (PrivateKey, error) {
+ priv, err := rsa.GenerateKey(rand.Reader, 4096)
+ if err != nil {
+ return nil, err
+ }
+ return RSAPrivateKey(priv), nil
+}
+
+func encodePrivateKey(privKey PrivateKey) ([]byte, error) {
+ return encodeKey(privKey, "private key")
+}
+
+// externally held key pairs
+
+type extPGPPrivateKey struct {
+ pubKey PublicKey
+ from string
+ pgpFingerprint string
+ bitLen int
+ doSign func(content []byte) ([]byte, error)
+}
+
+func newExtPGPPrivateKey(exportedPubKeyStream io.Reader, from string, sign func(content []byte) ([]byte, error)) (*extPGPPrivateKey, error) {
+ var pubKey *packet.PublicKey
+
+ rd := packet.NewReader(exportedPubKeyStream)
+ for {
+ pkt, err := rd.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("cannot read exported public key: %v", err)
+ }
+ cand, ok := pkt.(*packet.PublicKey)
+ if ok {
+ if cand.IsSubkey {
+ continue
+ }
+ if pubKey != nil {
+ return nil, fmt.Errorf("cannot select exported public key, found many")
+ }
+ pubKey = cand
+ }
+ }
+
+ if pubKey == nil {
+ return nil, fmt.Errorf("cannot read exported public key, found none (broken export)")
+
+ }
+
+ rsaPubKey, ok := pubKey.PublicKey.(*rsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("not a RSA key")
+ }
+
+ return &extPGPPrivateKey{
+ pubKey: RSAPublicKey(rsaPubKey),
+ from: from,
+ pgpFingerprint: fmt.Sprintf("%X", pubKey.Fingerprint),
+ bitLen: rsaPubKey.N.BitLen(),
+ doSign: sign,
+ }, nil
+}
+
+func (expk *extPGPPrivateKey) fingerprint() string {
+ return expk.pgpFingerprint
+}
+
+func (expk *extPGPPrivateKey) PublicKey() PublicKey {
+ return expk.pubKey
+}
+
+func (expk *extPGPPrivateKey) keyEncode(w io.Writer) error {
+ return fmt.Errorf("cannot access external private key to encode it")
+}
+
+func (expk *extPGPPrivateKey) sign(content []byte) (*packet.Signature, error) {
+ if expk.bitLen < 4096 {
+ return nil, fmt.Errorf("signing needs at least a 4096 bits key, got %d", expk.bitLen)
+ }
+
+ out, err := expk.doSign(content)
+ if err != nil {
+ return nil, err
+ }
+
+ badSig := fmt.Sprintf("bad %s produced signature: ", expk.from)
+
+ sigpkt, err := packet.Read(bytes.NewBuffer(out))
+ if err != nil {
+ return nil, fmt.Errorf(badSig+"%v", err)
+ }
+
+ sig, ok := sigpkt.(*packet.Signature)
+ if !ok {
+ return nil, fmt.Errorf(badSig+"got %T", sigpkt)
+ }
+
+ if sig.Hash != crypto.SHA512 {
+ return nil, fmt.Errorf(badSig + "expected SHA512 digest")
+ }
+
+ err = expk.pubKey.verify(content, sig)
+ if err != nil {
+ return nil, fmt.Errorf(badSig+"it does not verify: %v", err)
+ }
+
+ return sig, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package asserts implements snappy assertions and a database
+// abstraction for managing and holding them.
+package asserts
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "time"
+)
+
+// A Backstore stores assertions. It can store and retrieve assertions
+// by type under unique primary key headers (whose names are available
+// from assertType.PrimaryKey). Plus it supports searching by headers.
+// Lookups can be limited to a maximum allowed format.
+type Backstore interface {
+ // Put stores an assertion.
+ // It is responsible for checking that assert is newer than a
+ // previously stored revision with the same primary key headers.
+ Put(assertType *AssertionType, assert Assertion) error
+ // Get returns the assertion with the given unique key for its primary key headers.
+ // If none is present it returns ErrNotFound.
+ Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error)
+ // Search returns assertions matching the given headers.
+ // It invokes foundCb for each found assertion.
+ Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error
+}
+
+type nullBackstore struct{}
+
+func (nbs nullBackstore) Put(t *AssertionType, a Assertion) error {
+ return fmt.Errorf("cannot store assertions without setting a proper assertion backstore implementation")
+}
+
+func (nbs nullBackstore) Get(t *AssertionType, k []string, maxFormat int) (Assertion, error) {
+ return nil, ErrNotFound
+}
+
+func (nbs nullBackstore) Search(t *AssertionType, h map[string]string, f func(Assertion), maxFormat int) error {
+ return nil
+}
+
+// A KeypairManager is a manager and backstore for private/public key pairs.
+type KeypairManager interface {
+ // Put stores the given private/public key pair,
+ // making sure it can be later retrieved by its unique key id with Get.
+ // Trying to store a key with an already present key id should
+ // result in an error.
+ Put(privKey PrivateKey) error
+ // Get returns the private/public key pair with the given key id.
+ Get(keyID string) (PrivateKey, error)
+}
+
+// DatabaseConfig for an assertion database.
+type DatabaseConfig struct {
+ // trusted set of assertions (account and account-key supported)
+ Trusted []Assertion
+ // backstore for assertions, left unset storing assertions will error
+ Backstore Backstore
+ // manager/backstore for keypairs, defaults to in-memory implementation
+ KeypairManager KeypairManager
+ // assertion checkers used by Database.Check, left unset DefaultCheckers will be used which is recommended
+ Checkers []Checker
+}
+
+// Well-known errors
+var (
+ ErrNotFound = errors.New("assertion not found")
+)
+
+// RevisionError indicates a revision improperly used for an operation.
+type RevisionError struct {
+ Used, Current int
+}
+
+func (e *RevisionError) Error() string {
+ if e.Used < 0 || e.Current < 0 {
+ // TODO: message may need tweaking once there's a use.
+ return fmt.Sprintf("assertion revision is unknown")
+ }
+ if e.Used == e.Current {
+ return fmt.Sprintf("revision %d is already the current revision", e.Used)
+ }
+ if e.Used < e.Current {
+ return fmt.Sprintf("revision %d is older than current revision %d", e.Used, e.Current)
+ }
+ return fmt.Sprintf("revision %d is more recent than current revision %d", e.Used, e.Current)
+}
+
+// UnsupportedFormatError indicates an assertion with a format iteration not yet supported by the present version of asserts.
+type UnsupportedFormatError struct {
+ Ref *Ref
+ Format int
+ // Update marks there was already a current revision of the assertion and it has been kept.
+ Update bool
+}
+
+func (e *UnsupportedFormatError) Error() string {
+ postfx := ""
+ if e.Update {
+ postfx = " (current not updated)"
+ }
+ return fmt.Sprintf("proposed %q assertion has format %d but %d is latest supported%s", e.Ref.Type.Name, e.Format, e.Ref.Type.MaxSupportedFormat(), postfx)
+}
+
+// IsUnaccceptedUpdate returns whether the error indicates that an
+// assertion revision was already present and has been kept because
+// the update was not accepted.
+func IsUnaccceptedUpdate(err error) bool {
+ switch x := err.(type) {
+ case *UnsupportedFormatError:
+ return x.Update
+ case *RevisionError:
+ return x.Used <= x.Current
+ }
+ return false
+}
+
+// A RODatabase exposes read-only access to an assertion database.
+type RODatabase interface {
+ // IsTrustedAccount returns whether the account is part of the trusted set.
+ IsTrustedAccount(accountID string) bool
+ // Find an assertion based on arbitrary headers.
+ // Provided headers must contain the primary key for the assertion type.
+ // It returns ErrNotFound if the assertion cannot be found.
+ Find(assertionType *AssertionType, headers map[string]string) (Assertion, error)
+ // FindTrusted finds an assertion in the trusted set based on arbitrary headers.
+ // Provided headers must contain the primary key for the assertion type.
+ // It returns ErrNotFound if the assertion cannot be found.
+ FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error)
+ // FindMany finds assertions based on arbitrary headers.
+ // It returns ErrNotFound if no assertion can be found.
+ FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error)
+ // Check tests whether the assertion is properly signed and consistent with all the stored knowledge.
+ Check(assert Assertion) error
+}
+
+// A Checker defines a check on an assertion considering aspects such as
+// the signing key, and consistency with other
+// assertions in the database.
+type Checker func(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error
+
+// Database holds assertions and can be used to sign or check
+// further assertions.
+type Database struct {
+ bs Backstore
+ keypairMgr KeypairManager
+ trusted Backstore
+ backstores []Backstore
+ checkers []Checker
+}
+
+// OpenDatabase opens the assertion database based on the configuration.
+func OpenDatabase(cfg *DatabaseConfig) (*Database, error) {
+ bs := cfg.Backstore
+ keypairMgr := cfg.KeypairManager
+
+ if bs == nil {
+ bs = nullBackstore{}
+ }
+ if keypairMgr == nil {
+ keypairMgr = NewMemoryKeypairManager()
+ }
+
+ trustedBackstore := NewMemoryBackstore()
+
+ for _, a := range cfg.Trusted {
+ switch accepted := a.(type) {
+ case *AccountKey:
+ accKey := accepted
+ err := trustedBackstore.Put(AccountKeyType, accKey)
+ if err != nil {
+ return nil, fmt.Errorf("error loading for use trusted account key %q for %q: %v", accKey.PublicKeyID(), accKey.AccountID(), err)
+ }
+
+ case *Account:
+ acct := accepted
+ err := trustedBackstore.Put(AccountType, acct)
+ if err != nil {
+ return nil, fmt.Errorf("error loading for use trusted account %q: %v", acct.DisplayName(), err)
+ }
+ default:
+ return nil, fmt.Errorf("cannot load trusted assertions that are not account-key or account: %s", a.Type().Name)
+ }
+ }
+
+ checkers := cfg.Checkers
+ if len(checkers) == 0 {
+ checkers = DefaultCheckers
+ }
+ dbCheckers := make([]Checker, len(checkers))
+ copy(dbCheckers, checkers)
+
+ return &Database{
+ bs: bs,
+ keypairMgr: keypairMgr,
+ trusted: trustedBackstore,
+ // order here is relevant, Find* precedence and
+ // findAccountKey depend on it, trusted should win over the
+ // general backstore!
+ backstores: []Backstore{trustedBackstore, bs},
+ checkers: dbCheckers,
+ }, nil
+}
+
+// ImportKey stores the given private/public key pair.
+func (db *Database) ImportKey(privKey PrivateKey) error {
+ return db.keypairMgr.Put(privKey)
+}
+
+var (
+ // for sanity checking of base64 hash strings
+ base64HashLike = regexp.MustCompile("^[[:alnum:]_-]*$")
+)
+
+func (db *Database) safeGetPrivateKey(keyID string) (PrivateKey, error) {
+ if keyID == "" {
+ return nil, fmt.Errorf("key id is empty")
+ }
+ if !base64HashLike.MatchString(keyID) {
+ return nil, fmt.Errorf("key id contains unexpected chars: %q", keyID)
+ }
+ return db.keypairMgr.Get(keyID)
+}
+
+// PublicKey returns the public key part of the key pair that has the given key id.
+func (db *Database) PublicKey(keyID string) (PublicKey, error) {
+ privKey, err := db.safeGetPrivateKey(keyID)
+ if err != nil {
+ return nil, err
+ }
+ return privKey.PublicKey(), nil
+}
+
+// Sign assembles an assertion with the provided information and signs it
+// with the private key from `headers["authority-id"]` that has the provided key id.
+func (db *Database) Sign(assertType *AssertionType, headers map[string]interface{}, body []byte, keyID string) (Assertion, error) {
+ privKey, err := db.safeGetPrivateKey(keyID)
+ if err != nil {
+ return nil, err
+ }
+ return assembleAndSign(assertType, headers, body, privKey)
+}
+
+// findAccountKey finds an AccountKey exactly with account id and key id.
+func (db *Database) findAccountKey(authorityID, keyID string) (*AccountKey, error) {
+ key := []string{keyID}
+ // consider trusted account keys then disk stored account keys
+ for _, bs := range db.backstores {
+ a, err := bs.Get(AccountKeyType, key, AccountKeyType.MaxSupportedFormat())
+ if err == nil {
+ hit := a.(*AccountKey)
+ if hit.AccountID() != authorityID {
+ return nil, fmt.Errorf("found public key %q from %q but expected it from: %s", keyID, hit.AccountID(), authorityID)
+ }
+ return hit, nil
+ }
+ if err != ErrNotFound {
+ return nil, err
+ }
+ }
+ return nil, ErrNotFound
+}
+
+// IsTrustedAccount returns whether the account is part of the trusted set.
+func (db *Database) IsTrustedAccount(accountID string) bool {
+ if accountID == "" {
+ return false
+ }
+ _, err := db.trusted.Get(AccountType, []string{accountID}, AccountType.MaxSupportedFormat())
+ return err == nil
+}
+
+// Check tests whether the assertion is properly signed and consistent with all the stored knowledge.
+func (db *Database) Check(assert Assertion) error {
+ if !assert.SupportedFormat() {
+ return &UnsupportedFormatError{Ref: assert.Ref(), Format: assert.Format()}
+ }
+
+ typ := assert.Type()
+ now := time.Now()
+
+ var accKey *AccountKey
+ var err error
+ if typ.flags&noAuthority == 0 {
+ // TODO: later may need to consider type of assert to find candidate keys
+ accKey, err = db.findAccountKey(assert.AuthorityID(), assert.SignKeyID())
+ if err == ErrNotFound {
+ return fmt.Errorf("no matching public key %q for signature by %q", assert.SignKeyID(), assert.AuthorityID())
+ }
+ if err != nil {
+ return fmt.Errorf("error finding matching public key for signature: %v", err)
+ }
+ } else {
+ if assert.AuthorityID() != "" {
+ return fmt.Errorf("internal error: %q assertion cannot have authority-id set", typ.Name)
+ }
+ }
+
+ for _, checker := range db.checkers {
+ err := checker(assert, accKey, db, now)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Add persists the assertion after ensuring it is properly signed and consistent with all the stored knowledge.
+// It will return an error when trying to add an older revision of the assertion than the one currently stored.
+func (db *Database) Add(assert Assertion) error {
+ ref := assert.Ref()
+
+ if len(ref.PrimaryKey) == 0 {
+ return fmt.Errorf("internal error: assertion type %q has no primary key", ref.Type.Name)
+ }
+
+ err := db.Check(assert)
+ if err != nil {
+ if ufe, ok := err.(*UnsupportedFormatError); ok {
+ _, err := ref.Resolve(db.Find)
+ if err != nil && err != ErrNotFound {
+ return err
+ }
+ return &UnsupportedFormatError{Ref: ufe.Ref, Format: ufe.Format, Update: err == nil}
+ }
+ return err
+ }
+
+ for i, keyVal := range ref.PrimaryKey {
+ if keyVal == "" {
+ return fmt.Errorf("missing or non-string primary key header: %v", ref.Type.PrimaryKey[i])
+ }
+ }
+
+ // assuming trusted account keys/assertions will be managed
+ // through the os snap this seems the safest policy until we
+ // know more/better
+ _, err = db.trusted.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat())
+ if err != ErrNotFound {
+ return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted assertion: %v", ref.Type.Name, ref.PrimaryKey)
+ }
+
+ return db.bs.Put(ref.Type, assert)
+}
+
+func searchMatch(assert Assertion, expectedHeaders map[string]string) bool {
+ // check non-primary-key headers as well
+ for expectedKey, expectedValue := range expectedHeaders {
+ if assert.Header(expectedKey) != expectedValue {
+ return false
+ }
+ }
+ return true
+}
+
+func find(backstores []Backstore, assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) {
+ err := checkAssertType(assertionType)
+ if err != nil {
+ return nil, err
+ }
+ maxSupp := assertionType.MaxSupportedFormat()
+ if maxFormat == -1 {
+ maxFormat = maxSupp
+ } else {
+ if maxFormat > maxSupp {
+ return nil, fmt.Errorf("cannot find %q assertions for format %d higher than supported format %d", assertionType.Name, maxFormat, maxSupp)
+ }
+ }
+ keyValues := make([]string, len(assertionType.PrimaryKey))
+ for i, k := range assertionType.PrimaryKey {
+ keyVal := headers[k]
+ if keyVal == "" {
+ return nil, fmt.Errorf("must provide primary key: %v", k)
+ }
+ keyValues[i] = keyVal
+ }
+
+ var assert Assertion
+ for _, bs := range backstores {
+ a, err := bs.Get(assertionType, keyValues, maxFormat)
+ if err == nil {
+ assert = a
+ break
+ }
+ if err != ErrNotFound {
+ return nil, err
+ }
+ }
+
+ if assert == nil || !searchMatch(assert, headers) {
+ return nil, ErrNotFound
+ }
+
+ return assert, nil
+}
+
+// Find an assertion based on arbitrary headers.
+// Provided headers must contain the primary key for the assertion type.
+// It returns ErrNotFound if the assertion cannot be found.
+func (db *Database) Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) {
+ return find(db.backstores, assertionType, headers, -1)
+}
+
+// FindMaxFormat finds an assertion like Find but such that its
+// format is <= maxFormat by passing maxFormat along to the backend.
+// It returns ErrNotFound if such an assertion cannot be found.
+func (db *Database) FindMaxFormat(assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) {
+ return find(db.backstores, assertionType, headers, maxFormat)
+}
+
+// FindTrusted finds an assertion in the trusted set based on arbitrary headers.
+// Provided headers must contain the primary key for the assertion type.
+// It returns ErrNotFound if the assertion cannot be found.
+func (db *Database) FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) {
+ return find([]Backstore{db.trusted}, assertionType, headers, -1)
+}
+
+// FindMany finds assertions based on arbitrary headers.
+// It returns ErrNotFound if no assertion can be found.
+func (db *Database) FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) {
+ err := checkAssertType(assertionType)
+ if err != nil {
+ return nil, err
+ }
+ res := []Assertion{}
+
+ foundCb := func(assert Assertion) {
+ res = append(res, assert)
+ }
+
+ // TODO: Find variant taking this
+ maxFormat := assertionType.MaxSupportedFormat()
+ for _, bs := range db.backstores {
+ err = bs.Search(assertionType, headers, foundCb, maxFormat)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if len(res) == 0 {
+ return nil, ErrNotFound
+ }
+ return res, nil
+}
+
+// assertion checkers
+
+// CheckSigningKeyIsNotExpired checks that the signing key is not expired.
+func CheckSigningKeyIsNotExpired(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
+ if signingKey == nil {
+ // assert isn't signed with an account-key key, CheckSignature
+ // will fail anyway unless we teach it more stuff,
+ // Also this check isn't so relevant for self-signed asserts
+ // (e.g. account-key-request)
+ return nil
+ }
+ if !signingKey.isKeyValidAt(checkTime) {
+ return fmt.Errorf("assertion is signed with expired public key %q from %q", assert.SignKeyID(), assert.AuthorityID())
+ }
+ return nil
+}
+
+// CheckSignature checks that the signature is valid.
+func CheckSignature(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
+ var pubKey PublicKey
+ if signingKey != nil {
+ pubKey = signingKey.publicKey()
+ } else {
+ custom, ok := assert.(customSigner)
+ if !ok {
+ return fmt.Errorf("cannot check no-authority assertion type %q", assert.Type().Name)
+ }
+ pubKey = custom.signKey()
+ }
+ content, encSig := assert.Signature()
+ signature, err := decodeSignature(encSig)
+ if err != nil {
+ return err
+ }
+ err = pubKey.verify(content, signature)
+ if err != nil {
+ return fmt.Errorf("failed signature verification: %v", err)
+ }
+ return nil
+}
+
+type timestamped interface {
+ Timestamp() time.Time
+}
+
+// CheckTimestampVsSigningKeyValidity verifies that the timestamp of
+// the assertion is within the signing key validity.
+func CheckTimestampVsSigningKeyValidity(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
+ if signingKey == nil {
+ // assert isn't signed with an account-key key, CheckSignature
+ // will fail anyway unless we teach it more stuff.
+ // Also this check isn't so relevant for self-signed asserts
+ // (e.g. account-key-request)
+ return nil
+ }
+ if tstamped, ok := assert.(timestamped); ok {
+ checkTime := tstamped.Timestamp()
+ if !signingKey.isKeyValidAt(checkTime) {
+ return fmt.Errorf("%s assertion timestamp outside of signing key validity", assert.Type().Name)
+ }
+ }
+ return nil
+}
+
+// XXX: keeping these in this form until we know better
+
+// A consistencyChecker performs further checks based on the full
+// assertion database knowledge and its own signing key.
+type consistencyChecker interface {
+ checkConsistency(roDB RODatabase, signingKey *AccountKey) error
+}
+
+// CheckCrossConsistency verifies that the assertion is consistent with the other statements in the database.
+func CheckCrossConsistency(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
+ // see if the assertion requires further checks
+ if checker, ok := assert.(consistencyChecker); ok {
+ return checker.checkConsistency(roDB, signingKey)
+ }
+ return nil
+}
+
+// DefaultCheckers lists the default and recommended assertion
+// checkers used by Database if none are specified in the
+// DatabaseConfig.Checkers.
+var DefaultCheckers = []Checker{
+ CheckSigningKeyIsNotExpired,
+ CheckSignature,
+ CheckTimestampVsSigningKeyValidity,
+ CheckCrossConsistency,
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "bytes"
+ "crypto"
+ "encoding/base64"
+ "errors"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "testing"
+ "time"
+
+ "golang.org/x/crypto/openpgp/packet"
+ "golang.org/x/crypto/sha3"
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&openSuite{})
+var _ = Suite(&revisionErrorSuite{})
+
+type openSuite struct{}
+
+func (opens *openSuite) TestOpenDatabaseOK(c *C) {
+ cfg := &asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+ c.Assert(db, NotNil)
+}
+
+func (opens *openSuite) TestOpenDatabaseTrustedAccount(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "trusted",
+ "display-name": "Trusted",
+ "validation": "certified",
+ "timestamp": "2015-01-01T14:00:00Z",
+ }
+ acct, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, testPrivKey0)
+ c.Assert(err, IsNil)
+
+ cfg := &asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: []asserts.Assertion{acct},
+ }
+
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+
+ a, err := db.Find(asserts.AccountType, map[string]string{
+ "account-id": "trusted",
+ })
+ c.Assert(err, IsNil)
+ acct1 := a.(*asserts.Account)
+ c.Check(acct1.AccountID(), Equals, "trusted")
+ c.Check(acct1.DisplayName(), Equals, "Trusted")
+
+ c.Check(db.IsTrustedAccount("trusted"), Equals, true)
+
+ // empty account id (invalid) is not trusted
+ c.Check(db.IsTrustedAccount(""), Equals, false)
+}
+
+func (opens *openSuite) TestOpenDatabaseTrustedWrongType(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "0",
+ }
+ a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0)
+ c.Assert(err, IsNil)
+
+ cfg := &asserts.DatabaseConfig{
+ Trusted: []asserts.Assertion{a},
+ }
+
+ _, err = asserts.OpenDatabase(cfg)
+ c.Assert(err, ErrorMatches, "cannot load trusted assertions that are not account-key or account: test-only")
+}
+
+type databaseSuite struct {
+ topDir string
+ db *asserts.Database
+}
+
+var _ = Suite(&databaseSuite{})
+
+func (dbs *databaseSuite) SetUpTest(c *C) {
+ dbs.topDir = filepath.Join(c.MkDir(), "asserts-db")
+ fsKeypairMgr, err := asserts.OpenFSKeypairManager(dbs.topDir)
+ c.Assert(err, IsNil)
+ cfg := &asserts.DatabaseConfig{
+ KeypairManager: fsKeypairMgr,
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+ dbs.db = db
+}
+
+func (dbs *databaseSuite) TestImportKey(c *C) {
+ err := dbs.db.ImportKey(testPrivKey1)
+ c.Assert(err, IsNil)
+
+ keyPath := filepath.Join(dbs.topDir, "private-keys-v1", testPrivKey1SHA3_384)
+ info, err := os.Stat(keyPath)
+ c.Assert(err, IsNil)
+ c.Check(info.Mode().Perm(), Equals, os.FileMode(0600)) // secret
+ // too white box? ok at least until we have more functionality
+ privKey, err := ioutil.ReadFile(keyPath)
+ c.Assert(err, IsNil)
+
+ privKeyFromDisk, err := asserts.DecodePrivateKeyInTest(privKey)
+ c.Assert(err, IsNil)
+
+ c.Check(privKeyFromDisk.PublicKey().ID(), Equals, testPrivKey1SHA3_384)
+}
+
+func (dbs *databaseSuite) TestImportKeyAlreadyExists(c *C) {
+ err := dbs.db.ImportKey(testPrivKey1)
+ c.Assert(err, IsNil)
+
+ err = dbs.db.ImportKey(testPrivKey1)
+ c.Check(err, ErrorMatches, "key pair with given key id already exists")
+}
+
+func (dbs *databaseSuite) TestPublicKey(c *C) {
+ pk := testPrivKey1
+ keyID := pk.PublicKey().ID()
+ err := dbs.db.ImportKey(pk)
+ c.Assert(err, IsNil)
+
+ pubk, err := dbs.db.PublicKey(keyID)
+ c.Assert(err, IsNil)
+ c.Check(pubk.ID(), Equals, keyID)
+
+ // usual pattern is to then encode it
+ encoded, err := asserts.EncodePublicKey(pubk)
+ c.Assert(err, IsNil)
+ data, err := base64.StdEncoding.DecodeString(string(encoded))
+ c.Assert(err, IsNil)
+ c.Check(data[0], Equals, uint8(1)) // v1
+
+ // check details of packet
+ const newHeaderBits = 0x80 | 0x40
+ c.Check(data[1]&newHeaderBits, Equals, uint8(newHeaderBits))
+ c.Check(data[2] < 192, Equals, true) // small packet, 1 byte length
+ c.Check(data[3], Equals, uint8(4)) // openpgp v4
+ pkt, err := packet.Read(bytes.NewBuffer(data[1:]))
+ c.Assert(err, IsNil)
+ pubKey, ok := pkt.(*packet.PublicKey)
+ c.Assert(ok, Equals, true)
+ c.Check(pubKey.PubKeyAlgo, Equals, packet.PubKeyAlgoRSA)
+ c.Check(pubKey.IsSubkey, Equals, false)
+ fixedTimestamp := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)
+ c.Check(pubKey.CreationTime.Equal(fixedTimestamp), Equals, true)
+ // hash of blob content == hash of key
+ h384 := sha3.Sum384(data)
+ encHash := base64.RawURLEncoding.EncodeToString(h384[:])
+ c.Check(encHash, DeepEquals, testPrivKey1SHA3_384)
+}
+
+func (dbs *databaseSuite) TestPublicKeyNotFound(c *C) {
+ pk := testPrivKey1
+ keyID := pk.PublicKey().ID()
+
+ _, err := dbs.db.PublicKey(keyID)
+ c.Check(err, ErrorMatches, "cannot find key pair")
+
+ err = dbs.db.ImportKey(pk)
+ c.Assert(err, IsNil)
+
+ _, err = dbs.db.PublicKey("ff" + keyID)
+ c.Check(err, ErrorMatches, "cannot find key pair")
+}
+
+type checkSuite struct {
+ bs asserts.Backstore
+ a asserts.Assertion
+}
+
+var _ = Suite(&checkSuite{})
+
+func (chks *checkSuite) SetUpTest(c *C) {
+ var err error
+
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ chks.bs, err = asserts.OpenFSBackstore(topDir)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "0",
+ }
+ chks.a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0)
+ c.Assert(err, IsNil)
+}
+
+func (chks *checkSuite) TestCheckNoPubKey(c *C) {
+ cfg := &asserts.DatabaseConfig{
+ Backstore: chks.bs,
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+
+ err = db.Check(chks.a)
+ c.Assert(err, ErrorMatches, `no matching public key "[[:alnum:]_-]+" for signature by "canonical"`)
+}
+
+func (chks *checkSuite) TestCheckExpiredPubKey(c *C) {
+ trustedKey := testPrivKey0
+
+ cfg := &asserts.DatabaseConfig{
+ Backstore: chks.bs,
+ Trusted: []asserts.Assertion{asserts.ExpiredAccountKeyForTest("canonical", trustedKey.PublicKey())},
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+
+ err = db.Check(chks.a)
+ c.Assert(err, ErrorMatches, `assertion is signed with expired public key "[[:alnum:]_-]+" from "canonical"`)
+}
+
+func (chks *checkSuite) TestCheckForgery(c *C) {
+ trustedKey := testPrivKey0
+
+ cfg := &asserts.DatabaseConfig{
+ Backstore: chks.bs,
+ Trusted: []asserts.Assertion{asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey())},
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+
+ encoded := asserts.Encode(chks.a)
+ content, encodedSig := chks.a.Signature()
+ // forgery
+ forgedSig := new(packet.Signature)
+ forgedSig.PubKeyAlgo = packet.PubKeyAlgoRSA
+ forgedSig.Hash = crypto.SHA512
+ forgedSig.CreationTime = time.Now()
+ h := crypto.SHA512.New()
+ h.Write(content)
+ pk1 := packet.NewRSAPrivateKey(time.Unix(1, 0), testPrivKey1RSA)
+ err = forgedSig.Sign(h, pk1, &packet.Config{DefaultHash: crypto.SHA512})
+ c.Assert(err, IsNil)
+ buf := new(bytes.Buffer)
+ forgedSig.Serialize(buf)
+ b := append([]byte{0x1}, buf.Bytes()...)
+ forgedSigEncoded := base64.StdEncoding.EncodeToString(b)
+ forgedEncoded := bytes.Replace(encoded, encodedSig, []byte(forgedSigEncoded), 1)
+ c.Assert(forgedEncoded, Not(DeepEquals), encoded)
+
+ forgedAssert, err := asserts.Decode(forgedEncoded)
+ c.Assert(err, IsNil)
+
+ err = db.Check(forgedAssert)
+ c.Assert(err, ErrorMatches, "failed signature verification: .*")
+}
+
+func (chks *checkSuite) TestCheckUnsupportedFormat(c *C) {
+ trustedKey := testPrivKey0
+
+ cfg := &asserts.DatabaseConfig{
+ Backstore: chks.bs,
+ Trusted: []asserts.Assertion{asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey())},
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+
+ var a asserts.Assertion
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.TestOnlyType, 77)
+ defer restore()
+ var err error
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "0",
+ "format": "77",
+ }
+ a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, trustedKey)
+ c.Assert(err, IsNil)
+ })()
+
+ err = db.Check(a)
+ c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{})
+ c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported`)
+}
+
+type signAddFindSuite struct {
+ signingDB *asserts.Database
+ signingKeyID string
+ db *asserts.Database
+}
+
+var _ = Suite(&signAddFindSuite{})
+
+func (safs *signAddFindSuite) SetUpTest(c *C) {
+ cfg0 := &asserts.DatabaseConfig{}
+ db0, err := asserts.OpenDatabase(cfg0)
+ c.Assert(err, IsNil)
+ safs.signingDB = db0
+
+ pk := testPrivKey0
+ err = db0.ImportKey(pk)
+ c.Assert(err, IsNil)
+ safs.signingKeyID = pk.PublicKey().ID()
+
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, IsNil)
+
+ trustedKey := testPrivKey0
+ cfg := &asserts.DatabaseConfig{
+ Backstore: bs,
+ Trusted: []asserts.Assertion{
+ asserts.BootstrapAccountForTest("canonical"),
+ asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()),
+ },
+ }
+ db, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+ safs.db = db
+}
+
+func (safs *signAddFindSuite) TestSign(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Check(a1)
+ c.Check(err, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignEmptyKeyID(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, "")
+ c.Assert(err, ErrorMatches, "key id is empty")
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignMissingAuthorityId(c *C) {
+ headers := map[string]interface{}{
+ "primary-key": "a",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `"authority-id" header is mandatory`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignMissingPrimaryKey(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `"primary-key" header is mandatory`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignPrimaryKeyWithSlash(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "baz/9000",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `"primary-key" primary key header cannot contain '/'`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignNoPrivateKey(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, "abcd")
+ c.Assert(err, ErrorMatches, "cannot find key pair")
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignUnknownType(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ }
+ a1, err := safs.signingDB.Sign(&asserts.AssertionType{Name: "xyz", PrimaryKey: nil}, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `internal error: unknown assertion type: "xyz"`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignNonPredefinedType(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ }
+ a1, err := safs.signingDB.Sign(&asserts.AssertionType{Name: "test-only", PrimaryKey: nil}, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `internal error: unpredefined assertion type for name "test-only" used.*`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignBadRevision(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "revision": "zzz",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `"revision" header is not an integer: zzz`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignBadFormat(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "format": "zzz",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `"format" header is not an integer: zzz`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignHeadersCheck(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "extra": []interface{}{1, 2},
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Check(err, ErrorMatches, `header "extra": header values must be strings or nested lists or maps with strings as the only scalars: 1`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignHeadersCheckMap(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "extra": map[string]interface{}{"a": "a", "b": 1},
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Check(err, ErrorMatches, `header "extra": header values must be strings or nested lists or maps with strings as the only scalars: 1`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignAssemblerError(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "count": "zzz",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `cannot assemble assertion test-only: "count" header is not an integer: zzz`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestSignUnsupportedFormat(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "format": "77",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, ErrorMatches, `cannot sign "test-only" assertion with format 77 higher than max supported format 1`)
+ c.Check(a1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestAddSuperseding(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a1)
+ c.Assert(err, IsNil)
+
+ retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "a",
+ })
+ c.Assert(err, IsNil)
+ c.Check(retrieved1, NotNil)
+ c.Check(retrieved1.Revision(), Equals, 0)
+
+ headers["revision"] = "1"
+ a2, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a2)
+ c.Assert(err, IsNil)
+
+ retrieved2, err := safs.db.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "a",
+ })
+ c.Assert(err, IsNil)
+ c.Check(retrieved2, NotNil)
+ c.Check(retrieved2.Revision(), Equals, 1)
+
+ err = safs.db.Add(a1)
+ c.Check(err, ErrorMatches, "revision 0 is older than current revision 1")
+ c.Check(asserts.IsUnaccceptedUpdate(err), Equals, true)
+}
+
+func (safs *signAddFindSuite) TestAddNoAuthorityNoPrimaryKey(c *C) {
+ headers := map[string]interface{}{
+ "hdr": "FOO",
+ }
+ a, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, headers, nil, testPrivKey0)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a)
+ c.Assert(err, ErrorMatches, `internal error: assertion type "test-only-no-authority" has no primary key`)
+}
+
+func (safs *signAddFindSuite) TestAddNoAuthorityButPrimaryKey(c *C) {
+ headers := map[string]interface{}{
+ "pk": "primary",
+ }
+ a, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityPKType, headers, nil, testPrivKey0)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a)
+ c.Assert(err, ErrorMatches, `cannot check no-authority assertion type "test-only-no-authority-pk"`)
+}
+
+func (safs *signAddFindSuite) TestAddUnsupportedFormat(c *C) {
+ const unsupported = "type: test-only\n" +
+ "format: 77\n" +
+ "authority-id: canonical\n" +
+ "primary-key: a\n" +
+ "payload: unsupported\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ aUnsupp, err := asserts.Decode([]byte(unsupported))
+ c.Assert(err, IsNil)
+ c.Assert(aUnsupp.SupportedFormat(), Equals, false)
+
+ err = safs.db.Add(aUnsupp)
+ c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{})
+ c.Check(err.(*asserts.UnsupportedFormatError).Update, Equals, false)
+ c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported`)
+ c.Check(asserts.IsUnaccceptedUpdate(err), Equals, false)
+
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "format": "1",
+ "payload": "supported",
+ }
+ aSupp, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(aSupp)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(aUnsupp)
+ c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{})
+ c.Check(err.(*asserts.UnsupportedFormatError).Update, Equals, true)
+ c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported \(current not updated\)`)
+ c.Check(asserts.IsUnaccceptedUpdate(err), Equals, true)
+}
+
+func (safs *signAddFindSuite) TestFindNotFound(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a1)
+ c.Assert(err, IsNil)
+
+ retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "b",
+ })
+ c.Assert(err, Equals, asserts.ErrNotFound)
+ c.Check(retrieved1, IsNil)
+
+ // checking also extra headers
+ retrieved1, err = safs.db.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "a",
+ "authority-id": "other-auth-id",
+ })
+ c.Assert(err, Equals, asserts.ErrNotFound)
+ c.Check(retrieved1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestFindPrimaryLeftOut(c *C) {
+ retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{})
+ c.Assert(err, ErrorMatches, "must provide primary key: primary-key")
+ c.Check(retrieved1, IsNil)
+}
+
+func (safs *signAddFindSuite) TestFindMany(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "a",
+ "other": "other-x",
+ }
+ aa, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+ err = safs.db.Add(aa)
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "b",
+ "other": "other-y",
+ }
+ ab, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+ err = safs.db.Add(ab)
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "c",
+ "other": "other-x",
+ }
+ ac, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+ err = safs.db.Add(ac)
+ c.Assert(err, IsNil)
+
+ res, err := safs.db.FindMany(asserts.TestOnlyType, map[string]string{
+ "other": "other-x",
+ })
+ c.Assert(err, IsNil)
+ c.Assert(res, HasLen, 2)
+ primKeys := []string{res[0].HeaderString("primary-key"), res[1].HeaderString("primary-key")}
+ sort.Strings(primKeys)
+ c.Check(primKeys, DeepEquals, []string{"a", "c"})
+
+ res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{
+ "other": "other-y",
+ })
+ c.Assert(err, IsNil)
+ c.Assert(res, HasLen, 1)
+ c.Check(res[0].Header("primary-key"), Equals, "b")
+
+ res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{})
+ c.Assert(err, IsNil)
+ c.Assert(res, HasLen, 3)
+
+ res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{
+ "primary-key": "b",
+ "other": "other-y",
+ })
+ c.Assert(err, IsNil)
+ c.Assert(res, HasLen, 1)
+
+ res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{
+ "primary-key": "b",
+ "other": "other-x",
+ })
+ c.Assert(res, HasLen, 0)
+ c.Check(err, Equals, asserts.ErrNotFound)
+}
+
+func (safs *signAddFindSuite) TestFindFindsTrustedAccountKeys(c *C) {
+ pk1 := testPrivKey1
+
+ acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{
+ "authority-id": "canonical",
+ }, safs.signingKeyID)
+
+ acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{
+ "authority-id": "canonical",
+ }, pk1.PublicKey(), safs.signingKeyID)
+
+ err := safs.db.Add(acct1)
+ c.Assert(err, IsNil)
+ err = safs.db.Add(acct1Key)
+ c.Assert(err, IsNil)
+
+ // find the trusted key as well
+ tKey, err := safs.db.Find(asserts.AccountKeyType, map[string]string{
+ "account-id": "canonical",
+ "public-key-sha3-384": safs.signingKeyID,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical")
+ c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID)
+
+ // find trusted and indirectly trusted
+ accKeys, err := safs.db.FindMany(asserts.AccountKeyType, nil)
+ c.Assert(err, IsNil)
+ c.Check(accKeys, HasLen, 2)
+}
+
+func (safs *signAddFindSuite) TestFindTrusted(c *C) {
+ pk1 := testPrivKey1
+
+ acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{
+ "authority-id": "canonical",
+ }, safs.signingKeyID)
+
+ acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{
+ "authority-id": "canonical",
+ }, pk1.PublicKey(), safs.signingKeyID)
+
+ err := safs.db.Add(acct1)
+ c.Assert(err, IsNil)
+ err = safs.db.Add(acct1Key)
+ c.Assert(err, IsNil)
+
+ // find the trusted account
+ tAcct, err := safs.db.FindTrusted(asserts.AccountType, map[string]string{
+ "account-id": "canonical",
+ })
+ c.Assert(err, IsNil)
+ c.Assert(tAcct.(*asserts.Account).AccountID(), Equals, "canonical")
+
+ // find the trusted key
+ tKey, err := safs.db.FindTrusted(asserts.AccountKeyType, map[string]string{
+ "account-id": "canonical",
+ "public-key-sha3-384": safs.signingKeyID,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical")
+ c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID)
+
+ // doesn't find not trusted assertions
+ _, err = safs.db.FindTrusted(asserts.AccountType, map[string]string{
+ "account-id": acct1.AccountID(),
+ })
+ c.Check(err, Equals, asserts.ErrNotFound)
+
+ _, err = safs.db.FindTrusted(asserts.AccountKeyType, map[string]string{
+ "account-id": acct1.AccountID(),
+ "public-key-sha3-384": acct1Key.PublicKeyID(),
+ })
+ c.Check(err, Equals, asserts.ErrNotFound)
+}
+
+func (safs *signAddFindSuite) TestDontLetAddConfusinglyAssertionClashingWithTrustedOnes(c *C) {
+ // trusted
+ pubKey0, err := safs.signingDB.PublicKey(safs.signingKeyID)
+ c.Assert(err, IsNil)
+ pubKey0Encoded, err := asserts.EncodePublicKey(pubKey0)
+ c.Assert(err, IsNil)
+
+ now := time.Now().UTC()
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "canonical",
+ "public-key-sha3-384": safs.signingKeyID,
+ "name": "default",
+ "since": now.Format(time.RFC3339),
+ "until": now.AddDate(1, 0, 0).Format(time.RFC3339),
+ }
+ tKey, err := safs.signingDB.Sign(asserts.AccountKeyType, headers, []byte(pubKey0Encoded), safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(tKey)
+ c.Check(err, ErrorMatches, `cannot add "account-key" assertion with primary key clashing with a trusted assertion: .*`)
+}
+
+func (safs *signAddFindSuite) TestFindAndRefResolve(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "pk1": "ka",
+ "pk2": "kb",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnly2Type, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a1)
+ c.Assert(err, IsNil)
+
+ ref := &asserts.Ref{
+ Type: asserts.TestOnly2Type,
+ PrimaryKey: []string{"ka", "kb"},
+ }
+
+ resolved, err := ref.Resolve(safs.db.Find)
+ c.Assert(err, IsNil)
+ c.Check(resolved.Headers(), DeepEquals, map[string]interface{}{
+ "type": "test-only-2",
+ "authority-id": "canonical",
+ "pk1": "ka",
+ "pk2": "kb",
+ "sign-key-sha3-384": resolved.SignKeyID(),
+ })
+
+ ref = &asserts.Ref{
+ Type: asserts.TestOnly2Type,
+ PrimaryKey: []string{"kb", "ka"},
+ }
+ _, err = ref.Resolve(safs.db.Find)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+}
+
+func (safs *signAddFindSuite) TestFindMaxFormat(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "foo",
+ }
+ af0, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(af0)
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "foo",
+ "format": "1",
+ "revision": "1",
+ }
+ af1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(af1)
+ c.Assert(err, IsNil)
+
+ a, err := safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{
+ "primary-key": "foo",
+ }, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 1)
+
+ a, err = safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{
+ "primary-key": "foo",
+ }, 0)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+
+ a, err = safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{
+ "primary-key": "foo",
+ }, 3)
+ c.Check(err, ErrorMatches, `cannot find "test-only" assertions for format 3 higher than supported format 1`)
+}
+
+type revisionErrorSuite struct{}
+
+func (res *revisionErrorSuite) TestErrorText(c *C) {
+ tests := []struct {
+ err error
+ expected string
+ }{
+ // Invalid revisions.
+ {&asserts.RevisionError{Used: -1}, "assertion revision is unknown"},
+ {&asserts.RevisionError{Used: -100}, "assertion revision is unknown"},
+ {&asserts.RevisionError{Current: -1}, "assertion revision is unknown"},
+ {&asserts.RevisionError{Current: -100}, "assertion revision is unknown"},
+ {&asserts.RevisionError{Used: -1, Current: -1}, "assertion revision is unknown"},
+ // Used == Current.
+ {&asserts.RevisionError{}, "revision 0 is already the current revision"},
+ {&asserts.RevisionError{Used: 100, Current: 100}, "revision 100 is already the current revision"},
+ // Used < Current.
+ {&asserts.RevisionError{Used: 1, Current: 2}, "revision 1 is older than current revision 2"},
+ {&asserts.RevisionError{Used: 2, Current: 100}, "revision 2 is older than current revision 100"},
+ // Used > Current.
+ {&asserts.RevisionError{Current: 1, Used: 2}, "revision 2 is more recent than current revision 1"},
+ {&asserts.RevisionError{Current: 2, Used: 100}, "revision 100 is more recent than current revision 2"},
+ }
+
+ for _, test := range tests {
+ c.Check(test.err, ErrorMatches, test.expected)
+ }
+}
+
+type isUnacceptedUpdateSuite struct{}
+
+func (s *isUnacceptedUpdateSuite) TestIsUnacceptedUpdate(c *C) {
+ tests := []struct {
+ err error
+ keptCurrent bool
+ }{
+ {&asserts.UnsupportedFormatError{}, false},
+ {&asserts.UnsupportedFormatError{Update: true}, true},
+ {&asserts.RevisionError{Used: 1, Current: 1}, true},
+ {&asserts.RevisionError{Used: 1, Current: 5}, true},
+ {&asserts.RevisionError{Used: 3, Current: 1}, false},
+ {errors.New("other error"), false},
+ {asserts.ErrNotFound, false},
+ }
+
+ for _, t := range tests {
+ c.Check(asserts.IsUnaccceptedUpdate(t.err), Equals, t.keptCurrent, Commentf("%v", t.err))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+ "time"
+)
+
+// Model holds a model assertion, which is a statement by a brand
+// about the properties of a device model.
+type Model struct {
+ assertionBase
+ requiredSnaps []string
+ sysUserAuthority []string
+ timestamp time.Time
+}
+
+// BrandID returns the brand identifier. Same as the authority id.
+func (mod *Model) BrandID() string {
+ return mod.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier.
+func (mod *Model) Model() string {
+ return mod.HeaderString("model")
+}
+
+// DisplayName returns the human-friendly name of the model or
+// falls back to Model if this was not set.
+func (mod *Model) DisplayName() string {
+ display := mod.HeaderString("display-name")
+ if display == "" {
+ return mod.Model()
+ }
+ return display
+}
+
+// Series returns the series of the core software the model uses.
+func (mod *Model) Series() string {
+ return mod.HeaderString("series")
+}
+
+// Architecture returns the archicteture the model is based on.
+func (mod *Model) Architecture() string {
+ return mod.HeaderString("architecture")
+}
+
+// Gadget returns the gadget snap the model uses.
+func (mod *Model) Gadget() string {
+ return mod.HeaderString("gadget")
+}
+
+// Kernel returns the kernel snap the model uses.
+func (mod *Model) Kernel() string {
+ return mod.HeaderString("kernel")
+}
+
+// Store returns the snap store the model uses.
+func (mod *Model) Store() string {
+ return mod.HeaderString("store")
+}
+
+// RequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model.
+func (mod *Model) RequiredSnaps() []string {
+ return mod.requiredSnaps
+}
+
+// SystemUserAuthority returns the authority ids that are accepted as signers of system-user assertions for this model. Empty list means any.
+func (mod *Model) SystemUserAuthority() []string {
+ return mod.sysUserAuthority
+}
+
+// Timestamp returns the time when the model assertion was issued.
+func (mod *Model) Timestamp() time.Time {
+ return mod.timestamp
+}
+
+// Implement further consistency checks.
+func (mod *Model) checkConsistency(db RODatabase, acck *AccountKey) error {
+ // TODO: double check trust level of authority depending on class and possibly allowed-modes
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*Model)(nil)
+
+// limit model to only lowercase for now
+var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$")
+
+func checkModel(headers map[string]interface{}) (string, error) {
+ s, err := checkStringMatches(headers, "model", validModel)
+ if err != nil {
+ return "", err
+ }
+ // TODO: support the concept of case insensitive/preserving string headers
+ if strings.ToLower(s) != s {
+ return "", fmt.Errorf(`"model" header cannot contain uppercase letters`)
+ }
+ return s, nil
+}
+
+func checkAuthorityMatchesBrand(a Assertion) error {
+ typeName := a.Type().Name
+ authorityID := a.AuthorityID()
+ brand := a.HeaderString("brand-id")
+ if brand != authorityID {
+ return fmt.Errorf("authority-id and brand-id must match, %s assertions are expected to be signed by the brand: %q != %q", typeName, authorityID, brand)
+ }
+ return nil
+}
+
+var (
+ validAccountID = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28})$") // account ids look like snap-ids or are nice identifier
+)
+
+func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
+ const name = "system-user-authority"
+ v, ok := headers[name]
+ if !ok {
+ return []string{brandID}, nil
+ }
+ switch x := v.(type) {
+ case string:
+ if x == "*" {
+ return nil, nil
+ }
+ case []interface{}:
+ lst, err := checkStringListMatches(headers, name, validAccountID)
+ if err == nil {
+ return lst, nil
+ }
+ }
+ return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name)
+}
+
+var modelMandatory = []string{"architecture", "gadget", "kernel"}
+
+func assembleModel(assert assertionBase) (Assertion, error) {
+ err := checkAuthorityMatchesBrand(&assert)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, mandatory := range modelMandatory {
+ if _, err := checkNotEmptyString(assert.headers, mandatory); err != nil {
+ return nil, err
+ }
+ }
+
+ // store is optional but must be a string, defaults to the ubuntu store
+ _, err = checkOptionalString(assert.headers, "store")
+ if err != nil {
+ return nil, err
+ }
+
+ // display-name is optional but must be a string
+ _, err = checkOptionalString(assert.headers, "display-name")
+ if err != nil {
+ return nil, err
+ }
+
+ reqSnaps, err := checkStringList(assert.headers, "required-snaps")
+ if err != nil {
+ return nil, err
+ }
+
+ sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id"))
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ // NB:
+ // * core is not supported at this time, it defaults to ubuntu-core
+ // in prepare-image until rename and/or introduction of the header.
+ // * some form of allowed-modes, class are postponed,
+ //
+ // prepare-image takes care of not allowing them for now
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &Model{
+ assertionBase: assert,
+ requiredSnaps: reqSnaps,
+ sysUserAuthority: sysUserAuthority,
+ timestamp: timestamp,
+ }, nil
+}
+
+// Serial holds a serial assertion, which is a statement binding a
+// device identity with the device public key.
+type Serial struct {
+ assertionBase
+ timestamp time.Time
+ pubKey PublicKey
+}
+
+// BrandID returns the brand identifier of the device.
+func (ser *Serial) BrandID() string {
+ return ser.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier of the device.
+func (ser *Serial) Model() string {
+ return ser.HeaderString("model")
+}
+
+// Serial returns the serial identifier of the device, together with
+// brand id and model they form the unique identifier of the device.
+func (ser *Serial) Serial() string {
+ return ser.HeaderString("serial")
+}
+
+// DeviceKey returns the public key of the device.
+func (ser *Serial) DeviceKey() PublicKey {
+ return ser.pubKey
+}
+
+// Timestamp returns the time when the serial assertion was issued.
+func (ser *Serial) Timestamp() time.Time {
+ return ser.timestamp
+}
+
+// TODO: implement further consistency checks for Serial but first review approach
+
+func assembleSerial(assert assertionBase) (Assertion, error) {
+ err := checkAuthorityMatchesBrand(&assert)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
+ if err != nil {
+ return nil, err
+ }
+ pubKey, err := DecodePublicKey([]byte(encodedKey))
+ if err != nil {
+ return nil, err
+ }
+ keyID, err := checkNotEmptyString(assert.headers, "device-key-sha3-384")
+ if err != nil {
+ return nil, err
+ }
+ if keyID != pubKey.ID() {
+ return nil, fmt.Errorf("device key does not match provided key id")
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &Serial{
+ assertionBase: assert,
+ timestamp: timestamp,
+ pubKey: pubKey,
+ }, nil
+}
+
+// SerialRequest holds a serial-request assertion, which is a self-signed request to obtain a full device identity bound to the device public key.
+type SerialRequest struct {
+ assertionBase
+ pubKey PublicKey
+}
+
+// BrandID returns the brand identifier of the device making the request.
+func (sreq *SerialRequest) BrandID() string {
+ return sreq.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier of the device making the request.
+func (sreq *SerialRequest) Model() string {
+ return sreq.HeaderString("model")
+}
+
+// Serial returns the optional proposed serial identifier for the device, the service taking the request might use it or ignore it.
+func (sreq *SerialRequest) Serial() string {
+ return sreq.HeaderString("serial")
+}
+
+// RequestID returns the id for the request, obtained from and to be presented to the serial signing service.
+func (sreq *SerialRequest) RequestID() string {
+ return sreq.HeaderString("request-id")
+}
+
+// DeviceKey returns the public key of the device making the request.
+func (sreq *SerialRequest) DeviceKey() PublicKey {
+ return sreq.pubKey
+}
+
+func assembleSerialRequest(assert assertionBase) (Assertion, error) {
+ _, err := checkNotEmptyString(assert.headers, "brand-id")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "request-id")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkOptionalString(assert.headers, "serial")
+ if err != nil {
+ return nil, err
+ }
+
+ encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
+ if err != nil {
+ return nil, err
+ }
+ pubKey, err := DecodePublicKey([]byte(encodedKey))
+ if err != nil {
+ return nil, err
+ }
+
+ if pubKey.ID() != assert.SignKeyID() {
+ return nil, fmt.Errorf("device key does not match included signing key id")
+ }
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &SerialRequest{
+ assertionBase: assert,
+ pubKey: pubKey,
+ }, nil
+}
+
+// DeviceSessionRequest holds a device-session-request assertion, which is a request wrapping a store-provided nonce to start a session by a device signed with its key.
+type DeviceSessionRequest struct {
+ assertionBase
+ timestamp time.Time
+}
+
+// BrandID returns the brand identifier of the device making the request.
+func (req *DeviceSessionRequest) BrandID() string {
+ return req.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier of the device making the request.
+func (req *DeviceSessionRequest) Model() string {
+ return req.HeaderString("model")
+}
+
+// Serial returns the serial identifier of the device making the request,
+// together with brand id and model it forms the unique identifier of
+// the device.
+func (req *DeviceSessionRequest) Serial() string {
+ return req.HeaderString("serial")
+}
+
+// Nonce returns the nonce obtained from store and to be presented when requesting a device session.
+func (req *DeviceSessionRequest) Nonce() string {
+ return req.HeaderString("nonce")
+}
+
+// Timestamp returns the time when the device-session-request was created.
+func (req *DeviceSessionRequest) Timestamp() time.Time {
+ return req.timestamp
+}
+
+func assembleDeviceSessionRequest(assert assertionBase) (Assertion, error) {
+ _, err := checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "nonce")
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &DeviceSessionRequest{
+ assertionBase: assert,
+ timestamp: timestamp,
+ }, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type modelSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+var (
+ _ = Suite(&modelSuite{})
+ _ = Suite(&serialSuite{})
+)
+
+func (mods *modelSuite) SetUpSuite(c *C) {
+ mods.ts = time.Now().Truncate(time.Second).UTC()
+ mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n"
+}
+
+const (
+ reqSnaps = "required-snaps:\n - foo\n - bar\n"
+ sysUserAuths = "system-user-authority: *\n"
+)
+
+const modelExample = "type: model\n" +
+ "authority-id: brand-id1\n" +
+ "series: 16\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "display-name: Baz 3000\n" +
+ "architecture: amd64\n" +
+ "gadget: brand-gadget\n" +
+ "kernel: baz-linux\n" +
+ "store: brand-store\n" +
+ sysUserAuths +
+ reqSnaps +
+ "TSLINE" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (mods *modelSuite) TestDecodeOK(c *C) {
+ encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ model := a.(*asserts.Model)
+ c.Check(model.AuthorityID(), Equals, "brand-id1")
+ c.Check(model.Timestamp(), Equals, mods.ts)
+ c.Check(model.Series(), Equals, "16")
+ c.Check(model.BrandID(), Equals, "brand-id1")
+ c.Check(model.Model(), Equals, "baz-3000")
+ c.Check(model.DisplayName(), Equals, "Baz 3000")
+ c.Check(model.Architecture(), Equals, "amd64")
+ c.Check(model.Gadget(), Equals, "brand-gadget")
+ c.Check(model.Kernel(), Equals, "baz-linux")
+ c.Check(model.Store(), Equals, "brand-store")
+ c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"})
+ c.Check(model.SystemUserAuthority(), HasLen, 0)
+}
+
+func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.Store(), Equals, "")
+
+ encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ c.Check(model.Store(), Equals, "")
+}
+
+func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ // optional but we fallback to Model
+ c.Check(model.DisplayName(), Equals, "baz-3000")
+
+ encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ // optional but we fallback to Model
+ c.Check(model.DisplayName(), Equals, "baz-3000")
+}
+
+func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, reqSnaps, "", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.RequiredSnaps(), HasLen, 0)
+}
+
+func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ // the default is just to accept the brand itself
+ c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"})
+
+ encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n - foo\n - bar\n", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ c.Check(model.SystemUserAuthority(), DeepEquals, []string{"foo", "bar"})
+}
+
+const (
+ modelErrPrefix = "assertion model: "
+)
+
+func (mods *modelSuite) TestDecodeInvalid(c *C) {
+ encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"series: 16\n", "", `"series" header is mandatory`},
+ {"series: 16\n", "series: \n", `"series" header should not be empty`},
+ {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`},
+ {"model: baz-3000\n", "", `"model" header is mandatory`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`},
+ // lift this restriction at a later point
+ {"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`},
+ {"display-name: Baz 3000\n", "display-name:\n - xyz\n", `"display-name" header must be a string`},
+ {"architecture: amd64\n", "", `"architecture" header is mandatory`},
+ {"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`},
+ {"gadget: brand-gadget\n", "", `"gadget" header is mandatory`},
+ {"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`},
+ {"kernel: baz-linux\n", "", `"kernel" header is mandatory`},
+ {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`},
+ {"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`},
+ {mods.tsLine, "", `"timestamp" header is mandatory`},
+ {mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ {reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`},
+ {reqSnaps, "required-snaps:\n -\n - nested\n", `"required-snaps" header must be a list of strings`},
+ {sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`},
+ {sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
+ }
+}
+
+func (mods *modelSuite) TestModelCheck(c *C) {
+ ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+ brandDB := setup3rdPartySigning(c, "brand1", storeDB, db)
+
+ headers := ex.Headers()
+ headers["brand-id"] = brandDB.AuthorityID
+ headers["timestamp"] = time.Now().Format(time.RFC3339)
+ model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(model)
+ c.Assert(err, IsNil)
+}
+
+func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) {
+ ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+ brandDB := setup3rdPartySigning(c, "brand1", storeDB, db)
+
+ headers := ex.Headers()
+ headers["brand-id"] = brandDB.AuthorityID
+ headers["timestamp"] = "2011-01-01T14:00:00Z"
+ model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(model)
+ c.Assert(err, ErrorMatches, "model assertion timestamp outside of signing key validity")
+}
+
+type serialSuite struct {
+ ts time.Time
+ tsLine string
+ deviceKey asserts.PrivateKey
+ encodedDevKey string
+}
+
+func (ss *serialSuite) SetUpSuite(c *C) {
+ ss.ts = time.Now().Truncate(time.Second).UTC()
+ ss.tsLine = "timestamp: " + ss.ts.Format(time.RFC3339) + "\n"
+
+ ss.deviceKey = testPrivKey2
+ encodedPubKey, err := asserts.EncodePublicKey(ss.deviceKey.PublicKey())
+ c.Assert(err, IsNil)
+ ss.encodedDevKey = string(encodedPubKey)
+}
+
+const serialExample = "type: serial\n" +
+ "authority-id: brand-id1\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "serial: 2700\n" +
+ "device-key:\n DEVICEKEY\n" +
+ "device-key-sha3-384: KEYID\n" +
+ "TSLINE" +
+ "body-length: 2\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "HW" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (ss *serialSuite) TestDecodeOK(c *C) {
+ encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+ encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SerialType)
+ serial := a.(*asserts.Serial)
+ c.Check(serial.AuthorityID(), Equals, "brand-id1")
+ c.Check(serial.Timestamp(), Equals, ss.ts)
+ c.Check(serial.BrandID(), Equals, "brand-id1")
+ c.Check(serial.Model(), Equals, "baz-3000")
+ c.Check(serial.Serial(), Equals, "2700")
+ c.Check(serial.DeviceKey().ID(), Equals, ss.deviceKey.PublicKey().ID())
+}
+
+const (
+ deviceSessReqErrPrefix = "assertion device-session-request: "
+ serialErrPrefix = "assertion serial: "
+ serialProofErrPrefix = "assertion serial-proof: "
+ serialReqErrPrefix = "assertion serial-request: "
+)
+
+func (ss *serialSuite) TestDecodeInvalid(c *C) {
+ encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"authority-id: brand-id1\n", "authority-id: random\n", `authority-id and brand-id must match, serial assertions are expected to be signed by the brand: "random" != "brand-id1"`},
+ {"model: baz-3000\n", "", `"model" header is mandatory`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"serial: 2700\n", "", `"serial" header is mandatory`},
+ {"serial: 2700\n", "serial: \n", `"serial" header should not be empty`},
+ {ss.tsLine, "", `"timestamp" header is mandatory`},
+ {ss.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {ss.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
+ {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
+ {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`},
+ {"device-key-sha3-384: KEYID\n", "", `"device-key-sha3-384" header is mandatory`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ invalid = strings.Replace(invalid, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, serialErrPrefix+test.expectedErr)
+ }
+}
+
+func (ss *serialSuite) TestDecodeKeyIDMismatch(c *C) {
+ invalid := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+ invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ invalid = strings.Replace(invalid, "KEYID", "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", 1)
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, serialErrPrefix+"device key does not match provided key id")
+}
+
+func (ss *serialSuite) TestSerialRequestHappy(c *C) {
+ sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType,
+ map[string]interface{}{
+ "brand-id": "brand-id1",
+ "model": "baz-3000",
+ "device-key": ss.encodedDevKey,
+ "request-id": "REQID",
+ }, []byte("HW-DETAILS"), ss.deviceKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(sreq))
+ c.Assert(err, IsNil)
+
+ sreq2, ok := a.(*asserts.SerialRequest)
+ c.Assert(ok, Equals, true)
+
+ // standalone signature check
+ err = asserts.SignatureCheck(sreq2, sreq2.DeviceKey())
+ c.Check(err, IsNil)
+
+ c.Check(sreq2.BrandID(), Equals, "brand-id1")
+ c.Check(sreq2.Model(), Equals, "baz-3000")
+ c.Check(sreq2.RequestID(), Equals, "REQID")
+
+ c.Check(sreq2.Serial(), Equals, "")
+}
+
+func (ss *serialSuite) TestSerialRequestHappyOptionalSerial(c *C) {
+ sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType,
+ map[string]interface{}{
+ "brand-id": "brand-id1",
+ "model": "baz-3000",
+ "serial": "pserial",
+ "device-key": ss.encodedDevKey,
+ "request-id": "REQID",
+ }, []byte("HW-DETAILS"), ss.deviceKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(sreq))
+ c.Assert(err, IsNil)
+
+ sreq2, ok := a.(*asserts.SerialRequest)
+ c.Assert(ok, Equals, true)
+
+ c.Check(sreq2.Model(), Equals, "baz-3000")
+ c.Check(sreq2.Serial(), Equals, "pserial")
+}
+
+func (ss *serialSuite) TestSerialRequestDecodeInvalid(c *C) {
+ encoded := "type: serial-request\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "device-key:\n DEVICEKEY\n" +
+ "request-id: REQID\n" +
+ "serial: S\n" +
+ "body-length: 2\n" +
+ "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" +
+ "HW" +
+ "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"model: baz-3000\n", "", `"model" header is mandatory`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"request-id: REQID\n", "", `"request-id" header is mandatory`},
+ {"request-id: REQID\n", "request-id: \n", `"request-id" header should not be empty`},
+ {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
+ {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
+ {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`},
+ {"serial: S\n", "serial:\n - xyz\n", `"serial" header must be a string`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, serialReqErrPrefix+test.expectedErr)
+ }
+}
+
+func (ss *serialSuite) TestSerialRequestDecodeKeyIDMismatch(c *C) {
+ invalid := "type: serial-request\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "device-key:\n " + strings.Replace(ss.encodedDevKey, "\n", "\n ", -1) + "\n" +
+ "request-id: REQID\n" +
+ "body-length: 2\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "HW" +
+ "\n\n" +
+ "AXNpZw=="
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, "assertion serial-request: device key does not match included signing key id")
+}
+
+func (ss *serialSuite) TestDeviceSessionRequest(c *C) {
+ ts := time.Now().UTC().Round(time.Second)
+ sessReq, err := asserts.SignWithoutAuthority(asserts.DeviceSessionRequestType,
+ map[string]interface{}{
+ "brand-id": "brand-id1",
+ "model": "baz-3000",
+ "serial": "99990",
+ "nonce": "NONCE",
+ "timestamp": ts.Format(time.RFC3339),
+ }, nil, ss.deviceKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(sessReq))
+ c.Assert(err, IsNil)
+
+ sessReq2, ok := a.(*asserts.DeviceSessionRequest)
+ c.Assert(ok, Equals, true)
+
+ // standalone signature check
+ err = asserts.SignatureCheck(sessReq2, ss.deviceKey.PublicKey())
+ c.Check(err, IsNil)
+
+ c.Check(sessReq2.BrandID(), Equals, "brand-id1")
+ c.Check(sessReq2.Model(), Equals, "baz-3000")
+ c.Check(sessReq2.Serial(), Equals, "99990")
+ c.Check(sessReq2.Nonce(), Equals, "NONCE")
+ c.Check(sessReq2.Timestamp().Equal(ts), Equals, true)
+}
+
+func (ss *serialSuite) TestDeviceSessionRequestDecodeInvalid(c *C) {
+ tsLine := "timestamp: " + time.Now().Format(time.RFC3339) + "\n"
+ encoded := "type: device-session-request\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "serial: 99990\n" +
+ "nonce: NONCE\n" +
+ tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"serial: 99990\n", "", `"serial" header is mandatory`},
+ {"nonce: NONCE\n", "nonce: \n", `"nonce" header should not be empty`},
+ {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, deviceSessReqErrPrefix+test.expectedErr)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "crypto"
+ "encoding/base64"
+ "fmt"
+)
+
+// EncodeDigest encodes the digest from hash algorithm to be put in an assertion header.
+func EncodeDigest(hash crypto.Hash, hashDigest []byte) (string, error) {
+ algo := ""
+ switch hash {
+ case crypto.SHA512:
+ algo = "sha512"
+ case crypto.SHA3_384:
+ algo = "sha3-384"
+ default:
+ return "", fmt.Errorf("unsupported hash")
+ }
+ if len(hashDigest) != hash.Size() {
+ return "", fmt.Errorf("hash digest by %s should be %d bytes", algo, hash.Size())
+ }
+ return base64.RawURLEncoding.EncodeToString(hashDigest), nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "crypto"
+ _ "crypto/sha256"
+ "encoding/base64"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type encodeDigestSuite struct{}
+
+var _ = Suite(&encodeDigestSuite{})
+
+func (eds *encodeDigestSuite) TestEncodeDigestOK(c *C) {
+ h := crypto.SHA512.New()
+ h.Write([]byte("some stuff to hash"))
+ digest := h.Sum(nil)
+ encoded, err := asserts.EncodeDigest(crypto.SHA512, digest)
+ c.Assert(err, IsNil)
+
+ decoded, err := base64.RawURLEncoding.DecodeString(encoded)
+ c.Assert(err, IsNil)
+ c.Check(decoded, DeepEquals, digest)
+
+ // sha3-384
+ b, err := base64.RawURLEncoding.DecodeString(blobSHA3_384)
+ c.Assert(err, IsNil)
+ encoded, err = asserts.EncodeDigest(crypto.SHA3_384, b)
+ c.Assert(err, IsNil)
+ c.Check(encoded, Equals, blobSHA3_384)
+
+}
+
+func (eds *encodeDigestSuite) TestEncodeDigestErrors(c *C) {
+ _, err := asserts.EncodeDigest(crypto.SHA1, nil)
+ c.Check(err, ErrorMatches, "unsupported hash")
+
+ _, err = asserts.EncodeDigest(crypto.SHA512, []byte{1, 2})
+ c.Check(err, ErrorMatches, "hash digest by sha512 should be 64 bytes")
+
+ _, err = asserts.EncodeDigest(crypto.SHA3_384, []byte{1, 2})
+ c.Check(err, ErrorMatches, "hash digest by sha3-384 should be 48 bytes")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "io"
+ "time"
+)
+
+// expose test-only things here
+
+var NumAssertionType = len(typeRegistry)
+
+// v1FixedTimestamp exposed for tests
+var V1FixedTimestamp = v1FixedTimestamp
+
+// assembleAndSign exposed for tests
+var AssembleAndSignInTest = assembleAndSign
+
+// decodePrivateKey exposed for tests
+var DecodePrivateKeyInTest = decodePrivateKey
+
+// NewDecoderStressed makes a Decoder with a stressed setup with the given buffer and maximum sizes.
+func NewDecoderStressed(r io.Reader, bufSize, maxHeadersSize, maxBodySize, maxSigSize int) *Decoder {
+ return (&Decoder{
+ rd: r,
+ initialBufSize: bufSize,
+ maxHeadersSize: maxHeadersSize,
+ maxBodySize: maxBodySize,
+ maxSigSize: maxSigSize,
+ }).initBuffer()
+}
+
+// Encoder.append exposed for tests
+func EncoderAppend(enc *Encoder, encoded []byte) error {
+ return enc.append(encoded)
+}
+
+func BootstrapAccountForTest(authorityID string) *Account {
+ return &Account{
+ assertionBase: assertionBase{
+ headers: map[string]interface{}{
+ "type": "account",
+ "authority-id": authorityID,
+ "account-id": authorityID,
+ "validation": "certified",
+ },
+ },
+ timestamp: time.Now().UTC(),
+ }
+}
+
+func makeAccountKeyForTest(authorityID string, openPGPPubKey PublicKey, validYears int) *AccountKey {
+ return &AccountKey{
+ assertionBase: assertionBase{
+ headers: map[string]interface{}{
+ "type": "account-key",
+ "authority-id": authorityID,
+ "account-id": authorityID,
+ "public-key-sha3-384": openPGPPubKey.ID(),
+ },
+ },
+ since: time.Time{},
+ until: time.Time{}.UTC().AddDate(validYears, 0, 0),
+ pubKey: openPGPPubKey,
+ }
+}
+
+func BootstrapAccountKeyForTest(authorityID string, pubKey PublicKey) *AccountKey {
+ return makeAccountKeyForTest(authorityID, pubKey, 9999)
+}
+
+func ExpiredAccountKeyForTest(authorityID string, pubKey PublicKey) *AccountKey {
+ return makeAccountKeyForTest(authorityID, pubKey, 1)
+}
+
+// define dummy assertion types to use in the tests
+
+type TestOnly struct {
+ assertionBase
+}
+
+func assembleTestOnly(assert assertionBase) (Assertion, error) {
+ // for testing error cases
+ if _, err := checkIntWithDefault(assert.headers, "count", 0); err != nil {
+ return nil, err
+ }
+ return &TestOnly{assert}, nil
+}
+
+var TestOnlyType = &AssertionType{"test-only", []string{"primary-key"}, assembleTestOnly, 0}
+
+type TestOnly2 struct {
+ assertionBase
+}
+
+func assembleTestOnly2(assert assertionBase) (Assertion, error) {
+ return &TestOnly2{assert}, nil
+}
+
+var TestOnly2Type = &AssertionType{"test-only-2", []string{"pk1", "pk2"}, assembleTestOnly2, 0}
+
+type TestOnlyNoAuthority struct {
+ assertionBase
+}
+
+func assembleTestOnlyNoAuthority(assert assertionBase) (Assertion, error) {
+ if _, err := checkNotEmptyString(assert.headers, "hdr"); err != nil {
+ return nil, err
+ }
+ return &TestOnlyNoAuthority{assert}, nil
+}
+
+var TestOnlyNoAuthorityType = &AssertionType{"test-only-no-authority", nil, assembleTestOnlyNoAuthority, noAuthority}
+
+type TestOnlyNoAuthorityPK struct {
+ assertionBase
+}
+
+func assembleTestOnlyNoAuthorityPK(assert assertionBase) (Assertion, error) {
+ return &TestOnlyNoAuthorityPK{assert}, nil
+}
+
+var TestOnlyNoAuthorityPKType = &AssertionType{"test-only-no-authority-pk", []string{"pk"}, assembleTestOnlyNoAuthorityPK, noAuthority}
+
+func init() {
+ typeRegistry[TestOnlyType.Name] = TestOnlyType
+ maxSupportedFormat[TestOnlyType.Name] = 1
+ typeRegistry[TestOnly2Type.Name] = TestOnly2Type
+ typeRegistry[TestOnlyNoAuthorityType.Name] = TestOnlyNoAuthorityType
+ typeRegistry[TestOnlyNoAuthorityPKType.Name] = TestOnlyNoAuthorityPKType
+}
+
+// AccountKeyIsKeyValidAt exposes isKeyValidAt on AccountKey for tests
+func AccountKeyIsKeyValidAt(ak *AccountKey, when time.Time) bool {
+ return ak.isKeyValidAt(when)
+}
+
+type GPGRunner func(input []byte, args ...string) ([]byte, error)
+
+func MockRunGPG(mock func(prev GPGRunner, input []byte, args ...string) ([]byte, error)) (restore func()) {
+ prevRunGPG := runGPG
+ runGPG = func(input []byte, args ...string) ([]byte, error) {
+ return mock(prevRunGPG, input, args...)
+ }
+ return func() {
+ runGPG = prevRunGPG
+ }
+}
+
+// Headers helpers to test
+var (
+ ParseHeaders = parseHeaders
+ AppendEntry = appendEntry
+)
+
+// ParametersForGenerate exposes parametersForGenerate for tests.
+func (gkm *GPGKeypairManager) ParametersForGenerate(passphrase string, name string) string {
+ return gkm.parametersForGenerate(passphrase, name)
+}
+
+// ifacedecls tests
+var (
+ CompileAttributeConstraints = compileAttributeConstraints
+ CompilePlugRule = compilePlugRule
+ CompileSlotRule = compileSlotRule
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+)
+
+type fetchProgress int
+
+const (
+ fetchNotSeen fetchProgress = iota
+ fetchRetrieved
+ fetchSaved
+)
+
+// A Fetcher helps fetching assertions and their prerequisites.
+type Fetcher interface {
+ // Fetch retrieves the assertion indicated by ref then its prerequisites
+ // recursively, along the way saving prerequisites before dependent assertions.
+ Fetch(*Ref) error
+ // Save retrieves the prerequisites of the assertion recursively,
+ // along the way saving them, and finally saves the assertion.
+ Save(Assertion) error
+}
+
+type fetcher struct {
+ db RODatabase
+ retrieve func(*Ref) (Assertion, error)
+ save func(Assertion) error
+
+ fetched map[string]fetchProgress
+}
+
+// NewFetcher creates a Fetcher which will use trustedDB to determine trusted assertions, will fetch assertions following prerequisites using retrieve, and then will pass them to save, saving prerequisites before dependent assertions.
+func NewFetcher(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), save func(Assertion) error) Fetcher {
+ return &fetcher{
+ db: trustedDB,
+ retrieve: retrieve,
+ save: save,
+ fetched: make(map[string]fetchProgress),
+ }
+}
+
+func (f *fetcher) chase(ref *Ref, a Assertion) error {
+ // check if ref points to a trusted assertion, in which case
+ // there is nothing to do
+ _, err := ref.Resolve(f.db.FindTrusted)
+ if err == nil {
+ return nil
+ }
+ if err != ErrNotFound {
+ return err
+ }
+ u := ref.Unique()
+ switch f.fetched[u] {
+ case fetchSaved:
+ return nil // nothing to do
+ case fetchRetrieved:
+ return fmt.Errorf("internal error: circular assertions are not expected: %s", ref)
+ }
+ if a == nil {
+ retrieved, err := f.retrieve(ref)
+ if err != nil {
+ return err
+ }
+ a = retrieved
+ }
+ f.fetched[u] = fetchRetrieved
+ for _, preref := range a.Prerequisites() {
+ if err := f.Fetch(preref); err != nil {
+ return err
+ }
+ }
+ if err := f.fetchAccountKey(a.SignKeyID()); err != nil {
+ return err
+ }
+ if err := f.save(a); err != nil {
+ return err
+ }
+ f.fetched[u] = fetchSaved
+ return nil
+}
+
+// Fetch retrieves the assertion indicated by ref then its prerequisites
+// recursively, along the way saving prerequisites before dependent assertions.
+func (f *fetcher) Fetch(ref *Ref) error {
+ return f.chase(ref, nil)
+}
+
+// fetchAccountKey behaves like Fetch for the account-key with the given key id.
+func (f *fetcher) fetchAccountKey(keyID string) error {
+ keyRef := &Ref{
+ Type: AccountKeyType,
+ PrimaryKey: []string{keyID},
+ }
+ return f.Fetch(keyRef)
+}
+
+// Save retrieves the prerequisites of the assertion recursively,
+// along the way saving them, and finally saves the assertion.
+func (f *fetcher) Save(a Assertion) error {
+ return f.chase(a.Ref(), a)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "crypto"
+ "encoding/hex"
+ "fmt"
+ "time"
+
+ "golang.org/x/crypto/sha3"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+type fetcherSuite struct {
+ storeSigning *assertstest.StoreStack
+}
+
+var _ = Suite(&fetcherSuite{})
+
+func (s *fetcherSuite) SetUpTest(c *C) {
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+}
+
+func fakeSnap(rev int) []byte {
+ fake := fmt.Sprintf("hsqs________________%d", rev)
+ return []byte(fake)
+}
+
+func fakeHash(rev int) []byte {
+ h := sha3.Sum384(fakeSnap(rev))
+ return h[:]
+}
+
+func makeDigest(rev int) string {
+ d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev))
+ if err != nil {
+ panic(err)
+ }
+ return string(d)
+}
+
+func makeHexDigest(rev int) string {
+ return hex.EncodeToString(fakeHash(rev))
+}
+
+func (s *fetcherSuite) prereqSnapAssertions(c *C, revisions ...int) {
+ dev1Acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ err := s.storeSigning.Add(dev1Acct)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDecl)
+ c.Assert(err, IsNil)
+
+ for _, rev := range revisions {
+ headers = map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": makeDigest(rev),
+ "snap-size": "1000",
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapRev)
+ c.Assert(err, IsNil)
+ }
+}
+
+func (s *fetcherSuite) TestFetch(c *C) {
+ s.prereqSnapAssertions(c, 10)
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.storeSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+
+ ref := &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{makeDigest(10)},
+ }
+
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ return ref.Resolve(s.storeSigning.Find)
+ }
+
+ f := asserts.NewFetcher(db, retrieve, db.Add)
+
+ err = f.Fetch(ref)
+ c.Assert(err, IsNil)
+
+ snapRev, err := ref.Resolve(db.Find)
+ c.Assert(err, IsNil)
+ c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10)
+
+ snapDecl, err := db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ })
+ c.Assert(err, IsNil)
+ c.Check(snapDecl.(*asserts.SnapDeclaration).SnapName(), Equals, "foo")
+}
+
+func (s *fetcherSuite) TestSave(c *C) {
+ s.prereqSnapAssertions(c, 10)
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.storeSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ return ref.Resolve(s.storeSigning.Find)
+ }
+
+ f := asserts.NewFetcher(db, retrieve, db.Add)
+
+ ref := &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{makeDigest(10)},
+ }
+ rev, err := ref.Resolve(s.storeSigning.Find)
+ c.Assert(err, IsNil)
+
+ err = f.Save(rev)
+ c.Assert(err, IsNil)
+
+ snapRev, err := ref.Resolve(db.Find)
+ c.Assert(err, IsNil)
+ c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10)
+
+ snapDecl, err := db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ })
+ c.Assert(err, IsNil)
+ c.Check(snapDecl.(*asserts.SnapDeclaration).SnapName(), Equals, "foo")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+/*
+findWildcard invokes foundCb once for each parent directory of regular files matching:
+
+<top>/<descendantWithWildcard[0]>/<descendantWithWildcard[1]>...
+
+where each descendantWithWildcard component can contain the * wildcard;
+
+foundCb is invoked with the paths of the found regular files relative to top (that means top/ is excluded).
+
+Unlike filepath.Glob any I/O operation error stops the walking and bottoms out, so does a foundCb invocation that returns an error.
+*/
+func findWildcard(top string, descendantWithWildcard []string, foundCb func(relpath []string) error) error {
+ return findWildcardDescend(top, top, descendantWithWildcard, foundCb)
+}
+
+func findWildcardBottom(top, current string, pat string, names []string, foundCb func(relpath []string) error) error {
+ var hits []string
+ for _, name := range names {
+ ok, err := filepath.Match(pat, name)
+ if err != nil {
+ return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err)
+ }
+ if !ok {
+ continue
+ }
+ fn := filepath.Join(current, name)
+ finfo, err := os.Stat(fn)
+ if os.IsNotExist(err) {
+ continue
+ }
+ if err != nil {
+ return err
+ }
+ if !finfo.Mode().IsRegular() {
+ return fmt.Errorf("expected a regular file: %v", fn)
+ }
+ relpath, err := filepath.Rel(top, fn)
+ if err != nil {
+ return fmt.Errorf("findWildcard: unexpected to fail at computing rel path of descendant")
+ }
+ hits = append(hits, relpath)
+ }
+ if len(hits) == 0 {
+ return nil
+ }
+ return foundCb(hits)
+}
+
+func findWildcardDescend(top, current string, descendantWithWildcard []string, foundCb func(relpath []string) error) error {
+ k := descendantWithWildcard[0]
+ if len(descendantWithWildcard) > 1 && strings.IndexByte(k, '*') == -1 {
+ return findWildcardDescend(top, filepath.Join(current, k), descendantWithWildcard[1:], foundCb)
+ }
+
+ d, err := os.Open(current)
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ defer d.Close()
+ names, err := d.Readdirnames(-1)
+ if err != nil {
+ return err
+ }
+ if len(descendantWithWildcard) == 1 {
+ return findWildcardBottom(top, current, k, names, foundCb)
+ }
+ for _, name := range names {
+ ok, err := filepath.Match(k, name)
+ if err != nil {
+ return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err)
+ }
+ if ok {
+ err = findWildcardDescend(top, filepath.Join(current, name), descendantWithWildcard[1:], foundCb)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+
+ "gopkg.in/check.v1"
+)
+
+type findWildcardSuite struct{}
+
+var _ = check.Suite(&findWildcardSuite{})
+
+func (fs *findWildcardSuite) TestFindWildcard(c *check.C) {
+ top := filepath.Join(c.MkDir(), "top")
+
+ err := os.MkdirAll(top, os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id1"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id1", "abcd"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id1", "e5cd"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id2"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id2", "f444"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+
+ err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd", "active"), nil, os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd", "active.1"), nil, os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "e5cd", "active"), nil, os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = ioutil.WriteFile(filepath.Join(top, "acc-id2", "f444", "active"), nil, os.ModePerm)
+ c.Assert(err, check.IsNil)
+
+ var res []string
+ foundCb := func(relpath []string) error {
+ res = append(res, relpath...)
+ return nil
+ }
+
+ err = findWildcard(top, []string{"*", "*", "active"}, foundCb)
+ c.Assert(err, check.IsNil)
+ sort.Strings(res)
+ c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active", "acc-id2/f444/active"})
+
+ res = nil
+ err = findWildcard(top, []string{"*", "*", "active*"}, foundCb)
+ c.Assert(err, check.IsNil)
+ sort.Strings(res)
+ c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active", "acc-id2/f444/active"})
+
+ res = nil
+ err = findWildcard(top, []string{"zoo", "*", "active"}, foundCb)
+ c.Assert(err, check.IsNil)
+ c.Check(res, check.HasLen, 0)
+
+ res = nil
+ err = findWildcard(top, []string{"zoo", "*", "active*"}, foundCb)
+ c.Assert(err, check.IsNil)
+ c.Check(res, check.HasLen, 0)
+
+ res = nil
+ err = findWildcard(top, []string{"a*", "zoo", "active"}, foundCb)
+ c.Assert(err, check.IsNil)
+ c.Check(res, check.HasLen, 0)
+
+ res = nil
+ err = findWildcard(top, []string{"acc-id1", "*cd", "active"}, foundCb)
+ c.Assert(err, check.IsNil)
+ sort.Strings(res)
+ c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active"})
+
+ res = nil
+ err = findWildcard(top, []string{"acc-id1", "*cd", "active*"}, foundCb)
+ c.Assert(err, check.IsNil)
+ sort.Strings(res)
+ c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active"})
+
+}
+
+func (fs *findWildcardSuite) TestFindWildcardSomeErrors(c *check.C) {
+ top := filepath.Join(c.MkDir(), "top-errors")
+
+ err := os.MkdirAll(top, os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id1"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+ err = os.MkdirAll(filepath.Join(top, "acc-id2"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+
+ err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd"), nil, os.ModePerm)
+ c.Assert(err, check.IsNil)
+
+ err = os.MkdirAll(filepath.Join(top, "acc-id2", "dddd"), os.ModePerm)
+ c.Assert(err, check.IsNil)
+
+ var res []string
+ var retErr error
+ foundCb := func(relpath []string) error {
+ res = append(res, relpath...)
+ return retErr
+ }
+
+ myErr := errors.New("boom")
+ retErr = myErr
+ err = findWildcard(top, []string{"acc-id1", "*"}, foundCb)
+ c.Check(err, check.Equals, myErr)
+
+ retErr = nil
+ res = nil
+ err = findWildcard(top, []string{"acc-id2", "*"}, foundCb)
+ c.Check(err, check.ErrorMatches, "expected a regular file: .*")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+)
+
+// the default filesystem based backstore for assertions
+
+const (
+ assertionsLayoutVersion = "v0"
+ assertionsRoot = "asserts-" + assertionsLayoutVersion
+)
+
+type filesystemBackstore struct {
+ top string
+ mu sync.RWMutex
+}
+
+// OpenFSBackstore opens a filesystem backed assertions backstore under path.
+func OpenFSBackstore(path string) (Backstore, error) {
+ top := filepath.Join(path, assertionsRoot)
+ err := ensureTop(top)
+ if err != nil {
+ return nil, err
+ }
+ return &filesystemBackstore{top: top}, nil
+}
+
+// guarantees that result assertion is of the expected type (both in the AssertionType and go type sense)
+func (fsbs *filesystemBackstore) readAssertion(assertType *AssertionType, diskPrimaryPath string) (Assertion, error) {
+ encoded, err := readEntry(fsbs.top, assertType.Name, diskPrimaryPath)
+ if os.IsNotExist(err) {
+ return nil, ErrNotFound
+ }
+ if err != nil {
+ return nil, fmt.Errorf("broken assertion storage, cannot read assertion: %v", err)
+ }
+ assert, err := Decode(encoded)
+ if err != nil {
+ return nil, fmt.Errorf("broken assertion storage, cannot decode assertion: %v", err)
+ }
+ if assert.Type() != assertType {
+ return nil, fmt.Errorf("assertion that is not of type %q under their storage tree", assertType.Name)
+ }
+ // because of Decode() construction assert has also the expected go type
+ return assert, nil
+}
+
+func (fsbs *filesystemBackstore) pickLatestAssertion(assertType *AssertionType, diskPrimaryPaths []string, maxFormat int) (a Assertion, er error) {
+ for _, diskPrimaryPath := range diskPrimaryPaths {
+ fn := filepath.Base(diskPrimaryPath)
+ parts := strings.SplitN(fn, ".", 2)
+ formatnum := 0
+ if len(parts) == 2 {
+ var err error
+ formatnum, err = strconv.Atoi(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("invalid active assertion filename: %q", fn)
+ }
+ }
+ if formatnum <= maxFormat {
+ a1, err := fsbs.readAssertion(assertType, diskPrimaryPath)
+ if err != nil {
+ return nil, err
+ }
+ if a == nil || a1.Revision() > a.Revision() {
+ a = a1
+ }
+ }
+ }
+ if a == nil {
+ return nil, ErrNotFound
+ }
+ return a, nil
+}
+
+func diskPrimaryPathComps(primaryPath []string, active string) []string {
+ n := len(primaryPath)
+ comps := make([]string, n+1)
+ // safety against '/' etc
+ for i, comp := range primaryPath {
+ comps[i] = url.QueryEscape(comp)
+ }
+ comps[n] = active
+ return comps
+}
+
+func (fsbs *filesystemBackstore) currentAssertion(assertType *AssertionType, primaryPath []string, maxFormat int) (Assertion, error) {
+ var a Assertion
+ namesCb := func(relpaths []string) error {
+ var err error
+ a, err = fsbs.pickLatestAssertion(assertType, relpaths, maxFormat)
+ if err == ErrNotFound {
+ return nil
+ }
+ return err
+ }
+
+ comps := diskPrimaryPathComps(primaryPath, "active*")
+ assertTypeTop := filepath.Join(fsbs.top, assertType.Name)
+ err := findWildcard(assertTypeTop, comps, namesCb)
+ if err != nil {
+ return nil, fmt.Errorf("broken assertion storage, looking for %s: %v", assertType.Name, err)
+ }
+
+ if a == nil {
+ return nil, ErrNotFound
+ }
+
+ return a, nil
+}
+
+func (fsbs *filesystemBackstore) Put(assertType *AssertionType, assert Assertion) error {
+ fsbs.mu.Lock()
+ defer fsbs.mu.Unlock()
+
+ primaryPath := make([]string, len(assertType.PrimaryKey))
+ for i, k := range assertType.PrimaryKey {
+ primaryPath[i] = assert.HeaderString(k)
+ }
+
+ curAssert, err := fsbs.currentAssertion(assertType, primaryPath, assertType.MaxSupportedFormat())
+ if err == nil {
+ curRev := curAssert.Revision()
+ rev := assert.Revision()
+ if curRev >= rev {
+ return &RevisionError{Current: curRev, Used: rev}
+ }
+ } else if err != ErrNotFound {
+ return err
+ }
+
+ formatnum := assert.Format()
+ activeFn := "active"
+ if formatnum > 0 {
+ activeFn = fmt.Sprintf("active.%d", formatnum)
+ }
+ diskPrimaryPath := filepath.Join(diskPrimaryPathComps(primaryPath, activeFn)...)
+ err = atomicWriteEntry(Encode(assert), false, fsbs.top, assertType.Name, diskPrimaryPath)
+ if err != nil {
+ return fmt.Errorf("broken assertion storage, cannot write assertion: %v", err)
+ }
+ return nil
+}
+
+func (fsbs *filesystemBackstore) Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) {
+ fsbs.mu.RLock()
+ defer fsbs.mu.RUnlock()
+
+ return fsbs.currentAssertion(assertType, key, maxFormat)
+}
+
+func (fsbs *filesystemBackstore) search(assertType *AssertionType, diskPattern []string, foundCb func(Assertion), maxFormat int) error {
+ assertTypeTop := filepath.Join(fsbs.top, assertType.Name)
+ candCb := func(diskPrimaryPaths []string) error {
+ a, err := fsbs.pickLatestAssertion(assertType, diskPrimaryPaths, maxFormat)
+ if err == ErrNotFound {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ foundCb(a)
+ return nil
+ }
+ err := findWildcard(assertTypeTop, diskPattern, candCb)
+ if err != nil {
+ return fmt.Errorf("broken assertion storage, searching for %s: %v", assertType.Name, err)
+ }
+ return nil
+}
+
+func (fsbs *filesystemBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error {
+ fsbs.mu.RLock()
+ defer fsbs.mu.RUnlock()
+
+ n := len(assertType.PrimaryKey)
+ diskPattern := make([]string, n+1)
+ for i, k := range assertType.PrimaryKey {
+ keyVal := headers[k]
+ if keyVal == "" {
+ diskPattern[i] = "*"
+ } else {
+ diskPattern[i] = url.QueryEscape(keyVal)
+ }
+ }
+ diskPattern[n] = "active*"
+
+ candCb := func(a Assertion) {
+ if searchMatch(a, headers) {
+ foundCb(a)
+ }
+ }
+ return fsbs.search(assertType, diskPattern, candCb, maxFormat)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "os"
+ "path/filepath"
+ "syscall"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type fsBackstoreSuite struct{}
+
+var _ = Suite(&fsBackstoreSuite{})
+
+func (fsbss *fsBackstoreSuite) TestOpenOK(c *C) {
+ // ensure umask is clean when creating the DB dir
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Check(err, IsNil)
+ c.Check(bs, NotNil)
+
+ info, err := os.Stat(filepath.Join(topDir, "asserts-v0"))
+ c.Assert(err, IsNil)
+ c.Assert(info.IsDir(), Equals, true)
+ c.Check(info.Mode().Perm(), Equals, os.FileMode(0775))
+}
+
+func (fsbss *fsBackstoreSuite) TestOpenCreateFail(c *C) {
+ parent := filepath.Join(c.MkDir(), "var")
+ topDir := filepath.Join(parent, "asserts-db")
+ // make it not writable
+ err := os.Mkdir(parent, 0555)
+ c.Assert(err, IsNil)
+
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, ErrorMatches, "cannot create assert storage root: .*")
+ c.Check(bs, IsNil)
+}
+
+func (fsbss *fsBackstoreSuite) TestOpenWorldWritableFail(c *C) {
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ // make it world-writable
+ oldUmask := syscall.Umask(0)
+ os.MkdirAll(filepath.Join(topDir, "asserts-v0"), 0777)
+ syscall.Umask(oldUmask)
+
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*")
+ c.Check(bs, IsNil)
+}
+
+func (fsbss *fsBackstoreSuite) TestPutOldRevision(c *C) {
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, IsNil)
+
+ // Create two revisions of assertion.
+ a0, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ a1, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ // Put newer revision, follwed by old revision.
+ err = bs.Put(asserts.TestOnlyType, a1)
+ c.Assert(err, IsNil)
+ err = bs.Put(asserts.TestOnlyType, a0)
+
+ c.Check(err, ErrorMatches, `revision 0 is older than current revision 1`)
+ c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0})
+}
+
+func (fsbss *fsBackstoreSuite) TestGetFormat(c *C) {
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, IsNil)
+
+ af0, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ af1, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "format: 1\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ af2, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: zoo\n" +
+ "format: 2\n" +
+ "revision: 22\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ err = bs.Put(asserts.TestOnlyType, af0)
+ c.Assert(err, IsNil)
+ err = bs.Put(asserts.TestOnlyType, af1)
+ c.Assert(err, IsNil)
+
+ a, err := bs.Get(asserts.TestOnlyType, []string{"foo"}, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 1)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"foo"}, 0)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 0)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+
+ err = bs.Put(asserts.TestOnlyType, af2)
+ c.Assert(err, IsNil)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 2)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 22)
+}
+
+func (fsbss *fsBackstoreSuite) TestSearchFormat(c *C) {
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ bs, err := asserts.OpenFSBackstore(topDir)
+ c.Assert(err, IsNil)
+
+ af0, err := asserts.Decode([]byte("type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: foo\n" +
+ "pk2: bar\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ af1, err := asserts.Decode([]byte("type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: foo\n" +
+ "pk2: bar\n" +
+ "format: 1\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ af2, err := asserts.Decode([]byte("type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: foo\n" +
+ "pk2: baz\n" +
+ "format: 2\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ err = bs.Put(asserts.TestOnly2Type, af0)
+ c.Assert(err, IsNil)
+
+ queries := []map[string]string{
+ {"pk1": "foo", "pk2": "bar"},
+ {"pk1": "foo"},
+ {"pk2": "bar"},
+ }
+
+ for _, q := range queries {
+ var a asserts.Assertion
+ foundCb := func(a1 asserts.Assertion) {
+ a = a1
+ }
+ err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+ }
+
+ err = bs.Put(asserts.TestOnly2Type, af1)
+ c.Assert(err, IsNil)
+
+ for _, q := range queries {
+ var a asserts.Assertion
+ foundCb := func(a1 asserts.Assertion) {
+ a = a1
+ }
+ err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 1)
+
+ err = bs.Search(asserts.TestOnly2Type, q, foundCb, 0)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+ }
+
+ err = bs.Put(asserts.TestOnly2Type, af2)
+ c.Assert(err, IsNil)
+
+ var as []asserts.Assertion
+ foundCb := func(a1 asserts.Assertion) {
+ as = append(as, a1)
+ }
+ err = bs.Search(asserts.TestOnly2Type, map[string]string{
+ "pk1": "foo",
+ }, foundCb, 1) // will not find af2
+ c.Assert(err, IsNil)
+ c.Check(as, HasLen, 1)
+ c.Check(as[0].Revision(), Equals, 1)
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+// utilities to read/write fs entries
+
+func ensureTop(path string) error {
+ err := os.MkdirAll(path, 0775)
+ if err != nil {
+ return fmt.Errorf("cannot create assert storage root: %v", err)
+ }
+ info, err := os.Stat(path)
+ if err != nil {
+ return fmt.Errorf("cannot create assert storage root: %v", err)
+ }
+ if info.Mode().Perm()&0002 != 0 {
+ return fmt.Errorf("assert storage root unexpectedly world-writable: %v", path)
+ }
+ return nil
+}
+
+func atomicWriteEntry(data []byte, secret bool, top string, subpath ...string) error {
+ fpath := filepath.Join(top, filepath.Join(subpath...))
+ dir := filepath.Dir(fpath)
+ err := os.MkdirAll(dir, 0775)
+ if err != nil {
+ return err
+ }
+ fperm := 0664
+ if secret {
+ fperm = 0600
+ }
+ return osutil.AtomicWriteFile(fpath, data, os.FileMode(fperm), 0)
+}
+
+func entryExists(top string, subpath ...string) bool {
+ fpath := filepath.Join(top, filepath.Join(subpath...))
+ return osutil.FileExists(fpath)
+}
+
+func readEntry(top string, subpath ...string) ([]byte, error) {
+ fpath := filepath.Join(top, filepath.Join(subpath...))
+ return ioutil.ReadFile(fpath)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+// the default simple filesystem based keypair manager/backstore
+
+const (
+ privateKeysLayoutVersion = "v1"
+ privateKeysRoot = "private-keys-" + privateKeysLayoutVersion
+)
+
+type filesystemKeypairManager struct {
+ top string
+ mu sync.RWMutex
+}
+
+// OpenFSKeypairManager opens a filesystem backed assertions backstore under path.
+func OpenFSKeypairManager(path string) (KeypairManager, error) {
+ top := filepath.Join(path, privateKeysRoot)
+ err := ensureTop(top)
+ if err != nil {
+ return nil, err
+ }
+ return &filesystemKeypairManager{top: top}, nil
+}
+
+var errKeypairAlreadyExists = errors.New("key pair with given key id already exists")
+
+func (fskm *filesystemKeypairManager) Put(privKey PrivateKey) error {
+ keyID := privKey.PublicKey().ID()
+ if entryExists(fskm.top, keyID) {
+ return errKeypairAlreadyExists
+ }
+ encoded, err := encodePrivateKey(privKey)
+ if err != nil {
+ return fmt.Errorf("cannot store private key: %v", err)
+ }
+
+ fskm.mu.Lock()
+ defer fskm.mu.Unlock()
+
+ err = atomicWriteEntry(encoded, true, fskm.top, keyID)
+ if err != nil {
+ return fmt.Errorf("cannot store private key: %v", err)
+ }
+ return nil
+}
+
+var errKeypairNotFound = errors.New("cannot find key pair")
+
+func (fskm *filesystemKeypairManager) Get(keyID string) (PrivateKey, error) {
+ fskm.mu.RLock()
+ defer fskm.mu.RUnlock()
+
+ encoded, err := readEntry(fskm.top, keyID)
+ if os.IsNotExist(err) {
+ return nil, errKeypairNotFound
+ }
+ if err != nil {
+ return nil, fmt.Errorf("cannot read key pair: %v", err)
+ }
+ privKey, err := decodePrivateKey(encoded)
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode key pair: %v", err)
+ }
+ return privKey, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "os"
+ "path/filepath"
+ "syscall"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type fsKeypairMgrSuite struct{}
+
+var _ = Suite(&fsKeypairMgrSuite{})
+
+func (fsbss *fsKeypairMgrSuite) TestOpenOK(c *C) {
+ // ensure umask is clean when creating the DB dir
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ err := os.MkdirAll(topDir, 0775)
+ c.Assert(err, IsNil)
+
+ bs, err := asserts.OpenFSKeypairManager(topDir)
+ c.Check(err, IsNil)
+ c.Check(bs, NotNil)
+
+ info, err := os.Stat(filepath.Join(topDir, "private-keys-v1"))
+ c.Assert(err, IsNil)
+ c.Assert(info.IsDir(), Equals, true)
+ c.Check(info.Mode().Perm(), Equals, os.FileMode(0775))
+}
+
+func (fsbss *fsKeypairMgrSuite) TestOpenWorldWritableFail(c *C) {
+ topDir := filepath.Join(c.MkDir(), "asserts-db")
+ // make it world-writable
+ oldUmask := syscall.Umask(0)
+ os.MkdirAll(filepath.Join(topDir, "private-keys-v1"), 0777)
+ syscall.Umask(oldUmask)
+
+ bs, err := asserts.OpenFSKeypairManager(topDir)
+ c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*")
+ c.Check(bs, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+func ensureGPGHomeDirectory() (string, error) {
+ real, err := osutil.RealUser()
+ if err != nil {
+ return "", err
+ }
+
+ uid, err := strconv.Atoi(real.Uid)
+ if err != nil {
+ return "", err
+ }
+
+ gid, err := strconv.Atoi(real.Gid)
+ if err != nil {
+ return "", err
+ }
+
+ homedir := os.Getenv("SNAP_GNUPG_HOME")
+ if homedir == "" {
+ homedir = filepath.Join(real.HomeDir, ".snap", "gnupg")
+ }
+
+ if err := osutil.MkdirAllChown(homedir, 0700, uid, gid); err != nil {
+ return "", err
+ }
+ return homedir, nil
+}
+
+// findGPGCommand returns the path to a suitable GnuPG binary to use.
+// GnuPG 2 is mainly intended for desktop use, and is hard for us to use
+// here: in particular, it's extremely difficult to use it to delete a
+// secret key without a pinentry prompt (which would be necessary in our
+// test suite). GnuPG 1 is still supported so it's reasonable to continue
+// using that for now.
+func findGPGCommand() (string, error) {
+ if path := os.Getenv("SNAP_GNUPG_CMD"); path != "" {
+ return path, nil
+ }
+
+ path, err := exec.LookPath("gpg1")
+ if err != nil {
+ path, err = exec.LookPath("gpg")
+ }
+ return path, err
+}
+
+func runGPGImpl(input []byte, args ...string) ([]byte, error) {
+ homedir, err := ensureGPGHomeDirectory()
+ if err != nil {
+ return nil, err
+ }
+
+ // Ensure the gpg-agent knows what tty to talk to to ask for
+ // the passphrase. This is needed because we drive gpg over
+ // a pipe and if the agent is not already started it will
+ // fail to be able to ask for a password.
+ if os.Getenv("GPG_TTY") == "" {
+ tty, err := os.Readlink("/proc/self/fd/0")
+ if err != nil {
+ return nil, err
+ }
+ os.Setenv("GPG_TTY", tty)
+ }
+
+ general := []string{"--homedir", homedir, "-q", "--no-auto-check-trustdb"}
+ allArgs := append(general, args...)
+
+ path, err := findGPGCommand()
+ if err != nil {
+ return nil, err
+ }
+ cmd := exec.Command(path, allArgs...)
+ var outBuf bytes.Buffer
+ var errBuf bytes.Buffer
+
+ if len(input) != 0 {
+ cmd.Stdin = bytes.NewBuffer(input)
+ }
+
+ cmd.Stdout = &outBuf
+ cmd.Stderr = &errBuf
+
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("%s %s failed: %v (%q)", path, strings.Join(args, " "), err, errBuf.Bytes())
+ }
+
+ return outBuf.Bytes(), nil
+}
+
+var runGPG = runGPGImpl
+
+// A key pair manager backed by a local GnuPG setup.
+type GPGKeypairManager struct{}
+
+func (gkm *GPGKeypairManager) gpg(input []byte, args ...string) ([]byte, error) {
+ return runGPG(input, args...)
+}
+
+// NewGPGKeypairManager creates a new key pair manager backed by a local GnuPG setup.
+// Importing keys through the keypair manager interface is not
+// suppored.
+// Main purpose is allowing signing using keys from a GPG setup.
+func NewGPGKeypairManager() *GPGKeypairManager {
+ return &GPGKeypairManager{}
+}
+
+func (gkm *GPGKeypairManager) retrieve(fpr string) (PrivateKey, error) {
+ out, err := gkm.gpg(nil, "--batch", "--export", "--export-options", "export-minimal,export-clean,no-export-attributes", "0x"+fpr)
+ if err != nil {
+ return nil, err
+ }
+ if len(out) == 0 {
+ return nil, fmt.Errorf("cannot retrieve key with fingerprint %q in GPG keyring", fpr)
+ }
+
+ pubKeyBuf := bytes.NewBuffer(out)
+ privKey, err := newExtPGPPrivateKey(pubKeyBuf, "GPG", func(content []byte) ([]byte, error) {
+ return gkm.sign(fpr, content)
+ })
+ if err != nil {
+ return nil, fmt.Errorf("cannot load GPG public key with fingerprint %q: %v", fpr, err)
+ }
+ gotFingerprint := privKey.fingerprint()
+ if gotFingerprint != fpr {
+ return nil, fmt.Errorf("got wrong public key from GPG, expected fingerprint %q: %s", fpr, gotFingerprint)
+ }
+ return privKey, nil
+}
+
+// Walk iterates over all the RSA private keys in the local GPG setup calling the provided callback until this returns an error
+func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint string, uid string) error) error {
+ // see GPG source doc/DETAILS
+ out, err := gkm.gpg(nil, "--batch", "--list-secret-keys", "--fingerprint", "--with-colons", "--fixed-list-mode")
+ if err != nil {
+ return err
+ }
+ lines := strings.Split(string(out), "\n")
+ n := len(lines)
+ if n > 0 && lines[n-1] == "" {
+ n--
+ }
+ if n == 0 {
+ return nil
+ }
+ lines = lines[:n]
+ for j := 0; j < n; j++ {
+ // sec: line
+ line := lines[j]
+ if !strings.HasPrefix(line, "sec:") {
+ continue
+ }
+ secFields := strings.Split(line, ":")
+ if len(secFields) < 5 {
+ continue
+ }
+ if secFields[3] != "1" { // not RSA
+ continue
+ }
+ keyID := secFields[4]
+ uid := ""
+ fpr := ""
+ var privKey PrivateKey
+ // look for fpr:, uid: lines, order may vary and gpg2.1
+ // may springle additional lines in (like gpr:)
+ Loop:
+ for k := j + 1; k < n && !strings.HasPrefix(lines[k], "sec:"); k++ {
+ switch {
+ case strings.HasPrefix(lines[k], "fpr:"):
+ fprFields := strings.Split(lines[k], ":")
+ // extract "Field 10 - User-ID"
+ // A FPR record stores the fingerprint here.
+ if len(fprFields) < 10 {
+ break Loop
+ }
+ fpr = fprFields[9]
+ if !strings.HasSuffix(fpr, keyID) {
+ break // strange, skip
+ }
+ privKey, err = gkm.retrieve(fpr)
+ if err != nil {
+ return err
+ }
+ case strings.HasPrefix(lines[k], "uid:"):
+ uidFields := strings.Split(lines[k], ":")
+ // extract "*** Field 10 - User-ID"
+ if len(uidFields) < 10 {
+ break Loop
+ }
+ uid = uidFields[9]
+ }
+ }
+ // sanity checking
+ if privKey == nil || uid == "" {
+ continue
+ }
+ // collected it all
+ err = consider(privKey, fpr, uid)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (gkm *GPGKeypairManager) Put(privKey PrivateKey) error {
+ // NOTE: we don't need this initially at least and this keypair mgr is not for general arbitrary usage
+ return fmt.Errorf("cannot import private key into GPG keyring")
+}
+
+func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) {
+ stop := errors.New("stop marker")
+ var hit PrivateKey
+ match := func(privk PrivateKey, fpr string, uid string) error {
+ if privk.PublicKey().ID() == keyID {
+ hit = privk
+ return stop
+ }
+ return nil
+ }
+ err := gkm.Walk(match)
+ if err == stop {
+ return hit, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return nil, fmt.Errorf("cannot find key %q in GPG keyring", keyID)
+}
+
+func (gkm *GPGKeypairManager) sign(fingerprint string, content []byte) ([]byte, error) {
+ out, err := gkm.gpg(content, "--personal-digest-preferences", "SHA512", "--default-key", "0x"+fingerprint, "--detach-sign")
+ if err != nil {
+ return nil, fmt.Errorf("cannot sign using GPG: %v", err)
+ }
+ return out, nil
+}
+
+type gpgKeypairInfo struct {
+ privKey PrivateKey
+ fingerprint string
+}
+
+func (gkm *GPGKeypairManager) findByName(name string) (*gpgKeypairInfo, error) {
+ stop := errors.New("stop marker")
+ var hit *gpgKeypairInfo
+ match := func(privk PrivateKey, fpr string, uid string) error {
+ if uid == name {
+ hit = &gpgKeypairInfo{
+ privKey: privk,
+ fingerprint: fpr,
+ }
+ return stop
+ }
+ return nil
+ }
+ err := gkm.Walk(match)
+ if err == stop {
+ return hit, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return nil, fmt.Errorf("cannot find key named %q in GPG keyring", name)
+}
+
+// GetByName looks up a private key by name and returns it.
+func (gkm *GPGKeypairManager) GetByName(name string) (PrivateKey, error) {
+ keyInfo, err := gkm.findByName(name)
+ if err != nil {
+ return nil, err
+ }
+ return keyInfo.privKey, nil
+}
+
+var generateTemplate = `
+Key-Type: RSA
+Key-Length: 4096
+Name-Real: %s
+Creation-Date: seconds=%d
+Preferences: SHA512
+`
+
+func (gkm *GPGKeypairManager) parametersForGenerate(passphrase string, name string) string {
+ fixedCreationTime := v1FixedTimestamp.Unix()
+ generateParams := fmt.Sprintf(generateTemplate, name, fixedCreationTime)
+ if passphrase != "" {
+ generateParams += "Passphrase: " + passphrase + "\n"
+ }
+ return generateParams
+}
+
+// Generate creates a new key with the given passphrase and name.
+func (gkm *GPGKeypairManager) Generate(passphrase string, name string) error {
+ _, err := gkm.findByName(name)
+ if err == nil {
+ return fmt.Errorf("key named %q already exists in GPG keyring", name)
+ }
+ generateParams := gkm.parametersForGenerate(passphrase, name)
+ _, err = gkm.gpg([]byte(generateParams), "--batch", "--gen-key")
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Export returns the encoded text of the named public key.
+func (gkm *GPGKeypairManager) Export(name string) ([]byte, error) {
+ keyInfo, err := gkm.findByName(name)
+ if err != nil {
+ return nil, err
+ }
+ return EncodePublicKey(keyInfo.privKey.PublicKey())
+}
+
+// Delete removes the named key pair from GnuPG's storage.
+func (gkm *GPGKeypairManager) Delete(name string) error {
+ keyInfo, err := gkm.findByName(name)
+ if err != nil {
+ return err
+ }
+ _, err = gkm.gpg(nil, "--batch", "--delete-secret-and-public-key", "0x"+keyInfo.fingerprint)
+ if err != nil {
+ return err
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "fmt"
+ "os"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "golang.org/x/crypto/openpgp/packet"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type gpgKeypairMgrSuite struct {
+ homedir string
+ keypairMgr asserts.KeypairManager
+}
+
+var _ = Suite(&gpgKeypairMgrSuite{})
+
+func (gkms *gpgKeypairMgrSuite) SetUpSuite(c *C) {
+ if !osutil.FileExists("/usr/bin/gpg1") && !osutil.FileExists("/usr/bin/gpg") {
+ c.Skip("gpg not installed")
+ }
+}
+
+func (gkms *gpgKeypairMgrSuite) importKey(key string) {
+ assertstest.GPGImportKey(gkms.homedir, key)
+}
+
+func (gkms *gpgKeypairMgrSuite) SetUpTest(c *C) {
+ gkms.homedir = c.MkDir()
+ os.Setenv("SNAP_GNUPG_HOME", gkms.homedir)
+ gkms.keypairMgr = asserts.NewGPGKeypairManager()
+ // import test key
+ gkms.importKey(assertstest.DevKey)
+}
+
+func (gkms *gpgKeypairMgrSuite) TearDownTest(c *C) {
+ os.Unsetenv("SNAP_GNUPG_HOME")
+}
+
+func (gkms *gpgKeypairMgrSuite) TestGetPublicKeyLooksGood(c *C) {
+ got, err := gkms.keypairMgr.Get(assertstest.DevKeyID)
+ c.Assert(err, IsNil)
+ keyID := got.PublicKey().ID()
+ c.Check(keyID, Equals, assertstest.DevKeyID)
+}
+
+func (gkms *gpgKeypairMgrSuite) TestGetNotFound(c *C) {
+ got, err := gkms.keypairMgr.Get("ffffffffffffffff")
+ c.Check(err, ErrorMatches, `cannot find key "ffffffffffffffff" in GPG keyring`)
+ c.Check(got, IsNil)
+}
+
+func (gkms *gpgKeypairMgrSuite) TestUseInSigning(c *C) {
+ store := assertstest.NewStoreStack("trusted", testPrivKey0, testPrivKey1)
+
+ devKey, err := gkms.keypairMgr.Get(assertstest.DevKeyID)
+ c.Assert(err, IsNil)
+
+ devAcct := assertstest.NewAccount(store, "devel1", map[string]interface{}{
+ "account-id": "dev1-id",
+ }, "")
+ devAccKey := assertstest.NewAccountKey(store, devAcct, nil, devKey.PublicKey(), "")
+
+ signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: gkms.keypairMgr,
+ })
+ c.Assert(err, IsNil)
+
+ checkDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: store.Trusted,
+ })
+ c.Assert(err, IsNil)
+ // add store key
+ err = checkDB.Add(store.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ // enable devel key
+ err = checkDB.Add(devAcct)
+ c.Assert(err, IsNil)
+ err = checkDB.Add(devAccKey)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "authority-id": "dev1-id",
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "grade": "devel",
+ "snap-size": "1025",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapBuild, err := signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID)
+ c.Assert(err, IsNil)
+
+ err = checkDB.Check(snapBuild)
+ c.Check(err, IsNil)
+}
+
+func (gkms *gpgKeypairMgrSuite) TestGetNotUnique(c *C) {
+ mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) {
+ if args[1] == "--list-secret-keys" {
+ return prev(input, args...)
+ }
+ c.Assert(args[1], Equals, "--export")
+
+ pk1, err := rsa.GenerateKey(rand.Reader, 512)
+ c.Assert(err, IsNil)
+ pk2, err := rsa.GenerateKey(rand.Reader, 512)
+ c.Assert(err, IsNil)
+
+ buf := new(bytes.Buffer)
+ err = packet.NewRSAPublicKey(time.Now(), &pk1.PublicKey).Serialize(buf)
+ c.Assert(err, IsNil)
+ err = packet.NewRSAPublicKey(time.Now(), &pk2.PublicKey).Serialize(buf)
+ c.Assert(err, IsNil)
+
+ return buf.Bytes(), nil
+ }
+ restore := asserts.MockRunGPG(mockGPG)
+ defer restore()
+
+ _, err := gkms.keypairMgr.Get(assertstest.DevKeyID)
+ c.Check(err, ErrorMatches, `cannot load GPG public key with fingerprint "[A-F0-9]+": cannot select exported public key, found many`)
+}
+
+func (gkms *gpgKeypairMgrSuite) TestUseInSigningBrokenSignature(c *C) {
+ _, rsaPrivKey := assertstest.ReadPrivKey(assertstest.DevKey)
+ pgpPrivKey := packet.NewRSAPrivateKey(time.Unix(1, 0), rsaPrivKey)
+
+ var breakSig func(sig *packet.Signature, cont []byte) []byte
+
+ mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) {
+ if args[1] == "--list-secret-keys" || args[1] == "--export" {
+ return prev(input, args...)
+ }
+ n := len(args)
+ c.Assert(args[n-1], Equals, "--detach-sign")
+
+ sig := new(packet.Signature)
+ sig.PubKeyAlgo = packet.PubKeyAlgoRSA
+ sig.Hash = crypto.SHA512
+ sig.CreationTime = time.Now()
+
+ // poking to break the signature
+ cont := breakSig(sig, input)
+
+ h := sig.Hash.New()
+ h.Write([]byte(cont))
+
+ err := sig.Sign(h, pgpPrivKey, nil)
+ c.Assert(err, IsNil)
+
+ buf := new(bytes.Buffer)
+ sig.Serialize(buf)
+ return buf.Bytes(), nil
+ }
+ restore := asserts.MockRunGPG(mockGPG)
+ defer restore()
+
+ signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: gkms.keypairMgr,
+ })
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "authority-id": "dev1-id",
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "grade": "devel",
+ "snap-size": "1025",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+
+ tests := []struct {
+ breakSig func(*packet.Signature, []byte) []byte
+ expectedErr string
+ }{
+ {func(sig *packet.Signature, cont []byte) []byte {
+ sig.Hash = crypto.SHA1
+ return cont
+ }, "cannot sign assertion: bad GPG produced signature: expected SHA512 digest"},
+ {func(sig *packet.Signature, cont []byte) []byte {
+ return cont[:5]
+ }, "cannot sign assertion: bad GPG produced signature: it does not verify:.*"},
+ }
+
+ for _, t := range tests {
+ breakSig = t.breakSig
+
+ _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID)
+ c.Check(err, ErrorMatches, t.expectedErr)
+ }
+
+}
+
+func (gkms *gpgKeypairMgrSuite) TestUseInSigningFailure(c *C) {
+ mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) {
+ if args[1] == "--list-secret-keys" || args[1] == "--export" {
+ return prev(input, args...)
+ }
+ n := len(args)
+ c.Assert(args[n-1], Equals, "--detach-sign")
+ return nil, fmt.Errorf("boom")
+ }
+ restore := asserts.MockRunGPG(mockGPG)
+ defer restore()
+
+ signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: gkms.keypairMgr,
+ })
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "authority-id": "dev1-id",
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "grade": "devel",
+ "snap-size": "1025",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+
+ _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID)
+ c.Check(err, ErrorMatches, "cannot sign assertion: cannot sign using GPG: boom")
+}
+
+const shortPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1
+
+lQOYBFdGO7MBCADltsXglnDQdfBw0yOVpKZdkuvSnJKKn1H72PapgAr7ucLqNBCA
+js0kltDTa2LQP4vljiTyoMzOMnex4kXwRPlF+poZIEBHDLT0i/6sJ6mDukss1HBR
+GgNpU3y49WTXc8qxFY4clhbuqgQmy6bUmaVoo3Z4z7cqbsCepWfx5y+vJwMYqlo3
+Nb4q2+hTKS/o3yLiYB7/hkEhMZrFrOPR5SM7Tz5y7cpF6ObY+JZIp/MK+LsLWLji
+fEX/pcOtSjFdQqbcnhJJscXRERlFQDbc+gNmZYZ2RqdH5o46OliHkGhVDVTiW25A
+SqhGfnodypbZ9QAPSRvhLrN64AqEsvRb3I13ABEBAAEAB/9cQKg8Nz6sQUkkDm9C
+iCK1/qyNYwro9+3VXj9FOCJxEJuqMemUr4TMVnMcDQrchkC5GnpVJGXLw3HVcwFS
+amjPhUKAp7aYsg40DcrjuXP27oiFQvWuZGuNT5WNtCNg8WQr9POjIFWqWIYdTHk9
+9Ux79vW7s/Oj62GY9OWHPSilxpq1MjDKo9CSMbLeWxW+gbDxaD7cK7H/ONcz8bZ7
+pRfEhNIx3mEbWaZpWRrf+dSUx2OJbPGRkeFFMbCNapqftse173BZCwUKsW7RTp2S
+w8Vpo2Ky63Jlpz1DpoMDBz2vSH7pzaqAdnziI2r0IKiidajXFfpXJpJ3ICo/QhWj
+x1eRBADrI4I99zHeyy+12QMpkDrOu+ahF6/emdsm1FIy88TqeBmLkeXCXKZIpU3c
+USnxzm0nPNbOl7Nvf2VdAyeAftyag7t38Cud5MXldv/iY0e6oTKzxgha37yr6oRv
+PZ6VGwbkBvWti1HL4yx1QnkHFS6ailR9WiiHr3HaWAklZAsC0QQA+hgOi0V9fMZZ
+Y4/iFVRI9k1NK3pl0mP7pVTzbcjVYspLdIPQxPDsHJW0z48g23KOt0vL3yZvxdBx
+cfYGqIonAX19aMD5D4bNLx616pZs78DKGlOz6iXDcaib+n/uCNWxd5R/0m/zugrB
+qklpyIC/uxx+SmkJqqq378ytfvBMzccD/3Y6m3PM0ZnrIkr4Q7cKi9ao9rvM+J7o
+ziMgfnKWedNDxNa4tIVYYGPiXsjxY/ASUyxVjUPbkyCy3ubZrew0zQ9+kQbO/6vB
+WAg9ffT9M92QbSDjuxgUiC5GfvlCoDgJtuLRHd0YLDgUCS5nwb+teEsOpiNWEGXc
+Tr+5HZO+g6wxT6W0BiAoeHh4KYkBOAQTAQIAIgUCV0Y7swIbLwYLCQgHAwIGFQgC
+CQoLBBYCAwECHgECF4AACgkQEYacUJMr9p/i5wf/XbEiAe1+Y/ZNMO8PYnq1Nktk
+CbZEfQo+QH/9gJpt4p78YseWeUp14gsULLks3xRojlKNzYkqBpJcP7Ex+hQ3LEp7
+9IVbept5md4uuZcU0GFF42WAYXExd2cuxPv3lmWHOPuN63a/xpp0M2vYDfpt63qi
+Tly5/P4+NgpD6vAh8zwRHuBV/0mno/QX6cUCLVxq2v1aOqC9zq9B5sdYKQKjsQBP
+NOXCt1wPaINkqiW/8w2KhUl6mL6vhO0Onqu/F7M/YNXitv6Z2NFdFUVBh58UZW3C
+2jrc8JeRQ4Qlr1oeHh2loYOdZfxFPxRjhsRTnNKY8UHWLfbeI6lMqxR5G3DS+g==
+=kQRo
+-----END PGP PRIVATE KEY BLOCK-----
+`
+
+func (gkms *gpgKeypairMgrSuite) TestUseInSigningKeyTooShort(c *C) {
+ gkms.importKey(shortPrivKey)
+ privk, _ := assertstest.ReadPrivKey(shortPrivKey)
+
+ signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: gkms.keypairMgr,
+ })
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "authority-id": "dev1-id",
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "grade": "devel",
+ "snap-size": "1025",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+
+ _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, privk.PublicKey().ID())
+ c.Check(err, ErrorMatches, `cannot sign assertion: signing needs at least a 4096 bits key, got 2048`)
+}
+
+func (gkms *gpgKeypairMgrSuite) TestParametersForGenerate(c *C) {
+ gpgKeypairMgr := gkms.keypairMgr.(*asserts.GPGKeypairManager)
+ baseParameters := `
+Key-Type: RSA
+Key-Length: 4096
+Name-Real: test-key
+Creation-Date: seconds=1451606400
+Preferences: SHA512
+`
+
+ tests := []struct {
+ passphrase string
+ extraParameters string
+ }{
+ {"", ""},
+ {"secret", "Passphrase: secret\n"},
+ }
+
+ for _, test := range tests {
+ parameters := gpgKeypairMgr.ParametersForGenerate(test.passphrase, "test-key")
+ c.Check(parameters, Equals, baseParameters+test.extraParameters)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "crypto"
+ "encoding/base64"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// common checks used when decoding/assembling assertions
+
+func checkExistsString(headers map[string]interface{}, name string) (string, error) {
+ value, ok := headers[name]
+ if !ok {
+ return "", fmt.Errorf("%q header is mandatory", name)
+ }
+ s, ok := value.(string)
+ if !ok {
+ return "", fmt.Errorf("%q header must be a string", name)
+ }
+ return s, nil
+}
+
+func checkNotEmptyString(headers map[string]interface{}, name string) (string, error) {
+ s, err := checkExistsString(headers, name)
+ if err != nil {
+ return "", err
+ }
+ if len(s) == 0 {
+ return "", fmt.Errorf("%q header should not be empty", name)
+ }
+ return s, nil
+}
+
+func checkOptionalString(headers map[string]interface{}, name string) (string, error) {
+ value, ok := headers[name]
+ if !ok {
+ return "", nil
+ }
+ s, ok := value.(string)
+ if !ok {
+ return "", fmt.Errorf("%q header must be a string", name)
+ }
+ return s, nil
+}
+
+func checkPrimaryKey(headers map[string]interface{}, primKey string) (string, error) {
+ value, err := checkNotEmptyString(headers, primKey)
+ if err != nil {
+ return "", err
+ }
+ if strings.Contains(value, "/") {
+ return "", fmt.Errorf("%q primary key header cannot contain '/'", primKey)
+ }
+ return value, nil
+}
+
+func checkAssertType(assertType *AssertionType) error {
+ if assertType == nil {
+ return fmt.Errorf("internal error: assertion type cannot be nil")
+ }
+ // sanity check against known canonical
+ sanity := typeRegistry[assertType.Name]
+ switch sanity {
+ case assertType:
+ // fine, matches canonical
+ return nil
+ case nil:
+ return fmt.Errorf("internal error: unknown assertion type: %q", assertType.Name)
+ default:
+ return fmt.Errorf("internal error: unpredefined assertion type for name %q used (unexpected address %p)", assertType.Name, assertType)
+ }
+}
+
+// use 'defl' default if missing
+func checkIntWithDefault(headers map[string]interface{}, name string, defl int) (int, error) {
+ value, ok := headers[name]
+ if !ok {
+ return defl, nil
+ }
+ s, ok := value.(string)
+ if !ok {
+ return -1, fmt.Errorf("%q header is not an integer: %v", name, value)
+ }
+ m, err := strconv.Atoi(s)
+ if err != nil {
+ return -1, fmt.Errorf("%q header is not an integer: %v", name, s)
+ }
+ return m, nil
+}
+
+func checkInt(headers map[string]interface{}, name string) (int, error) {
+ valueStr, err := checkNotEmptyString(headers, name)
+ if err != nil {
+ return -1, err
+ }
+ value, err := strconv.Atoi(valueStr)
+ if err != nil {
+ return -1, fmt.Errorf("%q header is not an integer: %v", name, valueStr)
+ }
+ return value, nil
+}
+
+func checkRFC3339Date(headers map[string]interface{}, name string) (time.Time, error) {
+ dateStr, err := checkNotEmptyString(headers, name)
+ if err != nil {
+ return time.Time{}, err
+ }
+ date, err := time.Parse(time.RFC3339, dateStr)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("%q header is not a RFC3339 date: %v", name, err)
+ }
+ return date, nil
+}
+
+func checkRFC3339DateWithDefault(headers map[string]interface{}, name string, defl time.Time) (time.Time, error) {
+ value, ok := headers[name]
+ if !ok {
+ return defl, nil
+ }
+ dateStr, ok := value.(string)
+ if !ok {
+ return time.Time{}, fmt.Errorf("%q header must be a string", name)
+ }
+ date, err := time.Parse(time.RFC3339, dateStr)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("%q header is not a RFC3339 date: %v", name, err)
+ }
+ return date, nil
+}
+
+func checkUint(headers map[string]interface{}, name string, bitSize int) (uint64, error) {
+ valueStr, err := checkNotEmptyString(headers, name)
+ if err != nil {
+ return 0, err
+ }
+
+ value, err := strconv.ParseUint(valueStr, 10, bitSize)
+ if err != nil {
+ return 0, fmt.Errorf("%q header is not an unsigned integer: %v", name, valueStr)
+ }
+ return value, nil
+}
+
+func checkDigest(headers map[string]interface{}, name string, h crypto.Hash) ([]byte, error) {
+ digestStr, err := checkNotEmptyString(headers, name)
+ if err != nil {
+ return nil, err
+ }
+ b, err := base64.RawURLEncoding.DecodeString(digestStr)
+ if err != nil {
+ return nil, fmt.Errorf("%q header cannot be decoded: %v", name, err)
+ }
+ if len(b) != h.Size() {
+ return nil, fmt.Errorf("%q header does not have the expected bit length: %d", name, len(b)*8)
+ }
+
+ return b, nil
+}
+
+var anyString = regexp.MustCompile("")
+
+func checkStringListInMap(m map[string]interface{}, name, what string, pattern *regexp.Regexp) ([]string, error) {
+ value, ok := m[name]
+ if !ok {
+ return nil, nil
+ }
+ lst, ok := value.([]interface{})
+ if !ok {
+ return nil, fmt.Errorf("%s must be a list of strings", what)
+ }
+ if len(lst) == 0 {
+ return nil, nil
+ }
+ res := make([]string, len(lst))
+ for i, v := range lst {
+ s, ok := v.(string)
+ if !ok {
+ return nil, fmt.Errorf("%s must be a list of strings", what)
+ }
+ if !pattern.MatchString(s) {
+ return nil, fmt.Errorf("%s contains an invalid element: %q", what, s)
+ }
+ res[i] = s
+ }
+ return res, nil
+}
+
+func checkStringList(headers map[string]interface{}, name string) ([]string, error) {
+ return checkStringListMatches(headers, name, anyString)
+}
+
+func checkStringListMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) ([]string, error) {
+ return checkStringListInMap(headers, name, fmt.Sprintf("%q header", name), pattern)
+}
+
+func checkStringMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) (string, error) {
+ s, err := checkNotEmptyString(headers, name)
+ if err != nil {
+ return "", err
+ }
+ if !pattern.MatchString(s) {
+ return "", fmt.Errorf("%q header contains invalid characters: %q", name, s)
+ }
+ return s, nil
+}
+
+func checkOptionalBool(headers map[string]interface{}, name string) (bool, error) {
+ value, ok := headers[name]
+ if !ok {
+ return false, nil
+ }
+ s, ok := value.(string)
+ if !ok || (s != "true" && s != "false") {
+ return false, fmt.Errorf("%q header must be 'true' or 'false'", name)
+ }
+ return s == "true", nil
+}
+
+func checkMap(headers map[string]interface{}, name string) (map[string]interface{}, error) {
+ value, ok := headers[name]
+ if !ok {
+ return nil, nil
+ }
+ m, ok := value.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("%q header must be a map", name)
+ }
+ return m, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+ "sort"
+ "strings"
+ "unicode/utf8"
+)
+
+var (
+ nl = []byte("\n")
+ nlnl = []byte("\n\n")
+
+ // for basic sanity checking of header names
+ headerNameSanity = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$")
+)
+
+func parseHeaders(head []byte) (map[string]interface{}, error) {
+ if !utf8.Valid(head) {
+ return nil, fmt.Errorf("header is not utf8")
+ }
+ headers := make(map[string]interface{})
+ lines := strings.Split(string(head), "\n")
+ for i := 0; i < len(lines); {
+ entry := lines[i]
+ nameValueSplit := strings.Index(entry, ":")
+ if nameValueSplit == -1 {
+ return nil, fmt.Errorf("header entry missing ':' separator: %q", entry)
+ }
+ name := entry[:nameValueSplit]
+ if !headerNameSanity.MatchString(name) {
+ return nil, fmt.Errorf("invalid header name: %q", name)
+ }
+
+ consumed := nameValueSplit + 1
+ var value interface{}
+ var err error
+ value, i, err = parseEntry(consumed, i, lines, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, ok := headers[name]; ok {
+ return nil, fmt.Errorf("repeated header: %q", name)
+ }
+
+ headers[name] = value
+ }
+ return headers, nil
+}
+
+const (
+ commonPrefix = " "
+ multilinePrefix = " "
+ listChar = "-"
+ listPrefix = commonPrefix + listChar
+)
+
+func nestingPrefix(baseIndent int, prefix string) string {
+ return strings.Repeat(" ", baseIndent) + prefix
+}
+
+func parseEntry(consumedByIntro int, first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) {
+ entry := lines[first]
+ i := first + 1
+ if consumedByIntro == len(entry) {
+ // multiline values
+ basePrefix := nestingPrefix(baseIndent, commonPrefix)
+ if i < len(lines) && strings.HasPrefix(lines[i], basePrefix) {
+ rest := lines[i][len(basePrefix):]
+ if strings.HasPrefix(rest, listChar) {
+ // list
+ return parseList(i, lines, baseIndent)
+ }
+ if len(rest) > 0 && rest[0] != ' ' {
+ // map
+ return parseMap(i, lines, baseIndent)
+ }
+ }
+
+ return parseMultilineText(i, lines, baseIndent)
+ }
+
+ // simple one-line value
+ if entry[consumedByIntro] != ' ' {
+ return nil, -1, fmt.Errorf("header entry should have a space or newline (for multiline) before value: %q", entry)
+ }
+
+ return entry[consumedByIntro+1:], i, nil
+}
+
+func parseMultilineText(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) {
+ size := 0
+ i := first
+ j := i
+ prefix := nestingPrefix(baseIndent, multilinePrefix)
+ for j < len(lines) {
+ iline := lines[j]
+ if !strings.HasPrefix(iline, prefix) {
+ break
+ }
+ size += len(iline) - len(prefix) + 1
+ j++
+ }
+ if j == i {
+ var cur string
+ if i == len(lines) {
+ cur = "EOF"
+ } else {
+ cur = fmt.Sprintf("%q", lines[i])
+ }
+ return nil, -1, fmt.Errorf("expected %d chars nesting prefix after multiline introduction %q: %s", len(prefix), lines[i-1], cur)
+ }
+
+ valueBuf := bytes.NewBuffer(make([]byte, 0, size-1))
+ valueBuf.WriteString(lines[i][len(prefix):])
+ i++
+ for i < j {
+ valueBuf.WriteByte('\n')
+ valueBuf.WriteString(lines[i][len(prefix):])
+ i++
+ }
+
+ return valueBuf.String(), i, nil
+}
+
+func parseList(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) {
+ lst := []interface{}(nil)
+ j := first
+ prefix := nestingPrefix(baseIndent, listPrefix)
+ for j < len(lines) {
+ if !strings.HasPrefix(lines[j], prefix) {
+ return lst, j, nil
+ }
+ var v interface{}
+ var err error
+ v, j, err = parseEntry(len(prefix), j, lines, baseIndent+len(listPrefix)-1)
+ if err != nil {
+ return nil, -1, err
+ }
+ lst = append(lst, v)
+ }
+ return lst, j, nil
+}
+
+func parseMap(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) {
+ m := make(map[string]interface{})
+ j := first
+ prefix := nestingPrefix(baseIndent, commonPrefix)
+ for j < len(lines) {
+ if !strings.HasPrefix(lines[j], prefix) {
+ return m, j, nil
+ }
+
+ entry := lines[j][len(prefix):]
+ keyValueSplit := strings.Index(entry, ":")
+ if keyValueSplit == -1 {
+ return nil, -1, fmt.Errorf("map entry missing ':' separator: %q", entry)
+ }
+ key := entry[:keyValueSplit]
+ if !headerNameSanity.MatchString(key) {
+ return nil, -1, fmt.Errorf("invalid map entry key: %q", key)
+ }
+
+ consumed := keyValueSplit + 1
+ var value interface{}
+ var err error
+ value, j, err = parseEntry(len(prefix)+consumed, j, lines, len(prefix))
+ if err != nil {
+ return nil, -1, err
+ }
+
+ if _, ok := m[key]; ok {
+ return nil, -1, fmt.Errorf("repeated map entry: %q", key)
+ }
+
+ m[key] = value
+ }
+ return m, j, nil
+}
+
+// checkHeader checks that the header values are strings, or nested lists or maps with strings as the only scalars
+func checkHeader(v interface{}) error {
+ switch x := v.(type) {
+ case string:
+ return nil
+ case []interface{}:
+ for _, elem := range x {
+ err := checkHeader(elem)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ case map[string]interface{}:
+ for _, elem := range x {
+ err := checkHeader(elem)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ default:
+ return fmt.Errorf("header values must be strings or nested lists or maps with strings as the only scalars: %v", v)
+ }
+}
+
+// checkHeaders checks that headers are of expected types
+func checkHeaders(headers map[string]interface{}) error {
+ for name, value := range headers {
+ err := checkHeader(value)
+ if err != nil {
+ return fmt.Errorf("header %q: %v", name, err)
+ }
+ }
+ return nil
+}
+
+// copyHeader helps deep copying header values to defend against external mutations
+func copyHeader(v interface{}) interface{} {
+ switch x := v.(type) {
+ case string:
+ return x
+ case []interface{}:
+ res := make([]interface{}, len(x))
+ for i, elem := range x {
+ res[i] = copyHeader(elem)
+ }
+ return res
+ case map[string]interface{}:
+ res := make(map[string]interface{}, len(x))
+ for name, value := range x {
+ if value == nil {
+ continue // normalize nils out
+ }
+ res[name] = copyHeader(value)
+ }
+ return res
+ default:
+ panic(fmt.Sprintf("internal error: encountered unexpected value type copying headers: %v", v))
+ }
+}
+
+// copyHeader helps deep copying headers to defend against external mutations
+func copyHeaders(headers map[string]interface{}) map[string]interface{} {
+ return copyHeader(headers).(map[string]interface{})
+}
+
+func appendEntry(buf *bytes.Buffer, intro string, v interface{}, baseIndent int) {
+ switch x := v.(type) {
+ case nil:
+ return // omit
+ case string:
+ buf.WriteByte('\n')
+ buf.WriteString(intro)
+ if strings.IndexRune(x, '\n') != -1 {
+ // multiline value => quote by 4-space indenting
+ buf.WriteByte('\n')
+ pfx := nestingPrefix(baseIndent, multilinePrefix)
+ buf.WriteString(pfx)
+ x = strings.Replace(x, "\n", "\n"+pfx, -1)
+ } else {
+ buf.WriteByte(' ')
+ }
+ buf.WriteString(x)
+ case []interface{}:
+ if len(x) == 0 {
+ return // simply omit
+ }
+ buf.WriteByte('\n')
+ buf.WriteString(intro)
+ pfx := nestingPrefix(baseIndent, listPrefix)
+ for _, elem := range x {
+ appendEntry(buf, pfx, elem, baseIndent+len(listPrefix)-1)
+ }
+ case map[string]interface{}:
+ if len(x) == 0 {
+ return // simply omit
+ }
+ buf.WriteByte('\n')
+ buf.WriteString(intro)
+ // emit entries sorted by key
+ keys := make([]string, len(x))
+ i := 0
+ for key := range x {
+ keys[i] = key
+ i++
+ }
+ sort.Strings(keys)
+ pfx := nestingPrefix(baseIndent, commonPrefix)
+ for _, key := range keys {
+ appendEntry(buf, pfx+key+":", x[key], len(pfx))
+ }
+ default:
+ panic(fmt.Sprintf("internal error: encountered unexpected value type formatting headers: %v", v))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "bytes"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type headersSuite struct{}
+
+var _ = Suite(&headersSuite{})
+
+func (s *headersSuite) TestParseHeadersSimple(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`foo: 1
+bar: baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": "1",
+ "bar": "baz",
+ })
+}
+
+func (s *headersSuite) TestParseHeadersMultiline(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`foo:
+ abc
+
+bar: baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": "abc\n",
+ "bar": "baz",
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`foo: 1
+bar:
+ baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": "1",
+ "bar": "baz",
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`foo: 1
+bar:
+ baz
+ `))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": "1",
+ "bar": "baz\n",
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`foo: 1
+bar:
+ baz
+
+ baz2`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": "1",
+ "bar": "baz\n\nbaz2",
+ })
+}
+
+func (s *headersSuite) TestParseHeadersSimpleList(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`foo:
+ - x
+ - y
+ - z
+bar: baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": []interface{}{"x", "y", "z"},
+ "bar": "baz",
+ })
+}
+
+func (s *headersSuite) TestParseHeadersListNestedMultiline(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`foo:
+ - x
+ -
+ y1
+ y2
+
+ - z
+bar: baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": []interface{}{"x", "y1\ny2\n", "z"},
+ "bar": "baz",
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`bar: baz
+foo:
+ -
+ - u1
+ - u2
+ -
+ y1
+ y2
+ `))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": []interface{}{[]interface{}{"u1", "u2"}, "y1\ny2\n"},
+ "bar": "baz",
+ })
+}
+
+func (s *headersSuite) TestParseHeadersSimpleMap(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`foo:
+ x: X
+ yy: YY
+ z5:
+bar: baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": map[string]interface{}{
+ "x": "X",
+ "yy": "YY",
+ "z5": "",
+ },
+ "bar": "baz",
+ })
+}
+
+func (s *headersSuite) TestParseHeadersMapNestedMultiline(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`foo:
+ x: X
+ yy:
+ YY1
+ YY2
+ u:
+ - u1
+ - u2
+bar: baz`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "foo": map[string]interface{}{
+ "x": "X",
+ "yy": "YY1\nYY2",
+ "u": []interface{}{"u1", "u2"},
+ },
+ "bar": "baz",
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`one:
+ two:
+ three: `))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "one": map[string]interface{}{
+ "two": map[string]interface{}{
+ "three": "",
+ },
+ },
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`one:
+ two:
+ three`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "one": map[string]interface{}{
+ "two": "three",
+ },
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`map-within-map:
+ lev1:
+ lev2: x`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "map-within-map": map[string]interface{}{
+ "lev1": map[string]interface{}{
+ "lev2": "x",
+ },
+ },
+ })
+
+ m, err = asserts.ParseHeaders([]byte(`list-of-maps:
+ -
+ entry: foo
+ bar: baz
+ -
+ entry: bar`))
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "list-of-maps": []interface{}{
+ map[string]interface{}{
+ "entry": "foo",
+ "bar": "baz",
+ },
+ map[string]interface{}{
+ "entry": "bar",
+ },
+ },
+ })
+}
+
+func (s *headersSuite) TestParseHeadersMapErrors(c *C) {
+ _, err := asserts.ParseHeaders([]byte(`foo:
+ x X
+bar: baz`))
+ c.Check(err, ErrorMatches, `map entry missing ':' separator: "x X"`)
+
+ _, err = asserts.ParseHeaders([]byte(`foo:
+ 0x: X
+bar: baz`))
+ c.Check(err, ErrorMatches, `invalid map entry key: "0x"`)
+
+ _, err = asserts.ParseHeaders([]byte(`foo:
+ a: a
+ a: b`))
+ c.Check(err, ErrorMatches, `repeated map entry: "a"`)
+}
+
+func (s *headersSuite) TestParseHeadersErrors(c *C) {
+ _, err := asserts.ParseHeaders([]byte(`foo: 1
+bar:baz`))
+ c.Check(err, ErrorMatches, `header entry should have a space or newline \(for multiline\) before value: "bar:baz"`)
+
+ _, err = asserts.ParseHeaders([]byte(`foo:
+ - x
+ - y
+ - z
+bar: baz`))
+ c.Check(err, ErrorMatches, `expected 4 chars nesting prefix after multiline introduction "foo:": " - x"`)
+
+ _, err = asserts.ParseHeaders([]byte(`foo:
+ - x
+ - y
+ - z
+bar:`))
+ c.Check(err, ErrorMatches, `expected 4 chars nesting prefix after multiline introduction "bar:": EOF`)
+}
+
+func (s *headersSuite) TestAppendEntrySimple(c *C) {
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", "baz", 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": "baz",
+ })
+}
+
+func (s *headersSuite) TestAppendEntryMultiline(c *C) {
+ multilines := []string{
+ "a\n",
+ "a\nb",
+ "baz\n baz1\nbaz2",
+ "baz\n baz1\nbaz2\n",
+ "baz\n baz1\nbaz2\n\n",
+ }
+
+ for _, multiline := range multilines {
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", multiline, 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": multiline,
+ })
+ }
+}
+
+func (s *headersSuite) TestAppendEntrySimpleList(c *C) {
+ lst := []interface{}{"x", "y", "z"}
+
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", lst, 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": lst,
+ })
+}
+
+func (s *headersSuite) TestAppendEntryListNested(c *C) {
+ lst := []interface{}{"x", "a\nb\n", "", []interface{}{"u1", []interface{}{"w1", "w2"}}}
+
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", lst, 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": lst,
+ })
+}
+
+func (s *headersSuite) TestAppendEntrySimpleMap(c *C) {
+ mp := map[string]interface{}{
+ "x": "X",
+ "yy": "YY",
+ "z5": "",
+ }
+
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", mp, 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": mp,
+ })
+}
+
+func (s *headersSuite) TestAppendEntryNestedMap(c *C) {
+ mp := map[string]interface{}{
+ "x": "X",
+ "u": []interface{}{"u1", "u2"},
+ "yy": "YY1\nYY2",
+ "m": map[string]interface{}{"a": "A", "b": map[string]interface{}{"x": "X", "y": "Y"}},
+ }
+
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", mp, 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": mp,
+ })
+}
+
+func (s *headersSuite) TestAppendEntryOmitting(c *C) {
+ buf := bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", []interface{}{}, 0)
+
+ m, err := asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ })
+
+ lst := []interface{}{nil, []interface{}{}, "z"}
+
+ buf = bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", lst, 0)
+
+ m, err = asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ "bar": []interface{}{"z"},
+ })
+
+ buf = bytes.NewBufferString("start: .")
+
+ asserts.AppendEntry(buf, "bar:", map[string]interface{}{}, 0)
+
+ m, err = asserts.ParseHeaders(buf.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]interface{}{
+ "start": ".",
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+type attrMatcher interface {
+ match(context string, v interface{}) error
+}
+
+func chain(context, k string) string {
+ if context == "" {
+ return k
+ }
+ return fmt.Sprintf("%s.%s", context, k)
+}
+
+type compileContext struct {
+ dotted string
+ hadMap bool
+ wasAlt bool
+}
+
+func (cc compileContext) String() string {
+ return cc.dotted
+}
+
+func (cc compileContext) keyEntry(k string) compileContext {
+ return compileContext{
+ dotted: chain(cc.dotted, k),
+ hadMap: true,
+ wasAlt: false,
+ }
+}
+
+func (cc compileContext) alt(alt int) compileContext {
+ return compileContext{
+ dotted: fmt.Sprintf("%s/alt#%d/", cc.dotted, alt+1),
+ hadMap: cc.hadMap,
+ wasAlt: true,
+ }
+}
+
+// compileAttrMatcher compiles an attrMatcher derived from constraints,
+func compileAttrMatcher(cc compileContext, constraints interface{}) (attrMatcher, error) {
+ switch x := constraints.(type) {
+ case map[string]interface{}:
+ return compileMapAttrMatcher(cc, x)
+ case []interface{}:
+ if cc.wasAlt {
+ return nil, fmt.Errorf("cannot nest alternative constraints directly at %q", cc)
+ }
+ return compileAltAttrMatcher(cc, x)
+ case string:
+ if !cc.hadMap {
+ return nil, fmt.Errorf("first level of non alternative constraints must be a set of key-value contraints")
+ }
+ return compileRegexpAttrMatcher(cc, x)
+ default:
+ return nil, fmt.Errorf("constraint %q must be a key-value map, regexp or a list of alternative constraints: %v", cc, x)
+ }
+}
+
+type mapAttrMatcher map[string]attrMatcher
+
+func compileMapAttrMatcher(cc compileContext, m map[string]interface{}) (attrMatcher, error) {
+ matcher := make(mapAttrMatcher)
+ for k, constraint := range m {
+ matcher1, err := compileAttrMatcher(cc.keyEntry(k), constraint)
+ if err != nil {
+ return nil, err
+ }
+ matcher[k] = matcher1
+ }
+ return matcher, nil
+}
+
+func matchEntry(context, k string, matcher1 attrMatcher, v interface{}) error {
+ context = chain(context, k)
+ if v == nil {
+ return fmt.Errorf("attribute %q has constraints but is unset", context)
+ }
+ if err := matcher1.match(context, v); err != nil {
+ return err
+ }
+ return nil
+}
+
+func matchList(context string, matcher attrMatcher, l []interface{}) error {
+ for i, elem := range l {
+ if err := matcher.match(chain(context, strconv.Itoa(i)), elem); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (matcher mapAttrMatcher) match(context string, v interface{}) error {
+ switch x := v.(type) {
+ case map[string]interface{}: // maps in attributes look like this
+ for k, matcher1 := range matcher {
+ if err := matchEntry(context, k, matcher1, x[k]); err != nil {
+ return err
+ }
+ }
+ case []interface{}:
+ return matchList(context, matcher, x)
+ default:
+ return fmt.Errorf("attribute %q must be a map", context)
+ }
+ return nil
+}
+
+type regexpAttrMatcher struct {
+ *regexp.Regexp
+}
+
+func compileRegexpAttrMatcher(cc compileContext, s string) (attrMatcher, error) {
+ rx, err := regexp.Compile("^(" + s + ")$")
+ if err != nil {
+ return nil, fmt.Errorf("cannot compile %q constraint %q: %v", cc, s, err)
+ }
+ return regexpAttrMatcher{rx}, nil
+}
+
+func (matcher regexpAttrMatcher) match(context string, v interface{}) error {
+ var s string
+ switch x := v.(type) {
+ case string:
+ s = x
+ case bool:
+ s = strconv.FormatBool(x)
+ case int64:
+ s = strconv.FormatInt(x, 10)
+ case []interface{}:
+ return matchList(context, matcher, x)
+ default:
+ return fmt.Errorf("attribute %q must be a scalar or list", context)
+ }
+ if !matcher.Regexp.MatchString(s) {
+ return fmt.Errorf("attribute %q value %q does not match %v", context, s, matcher.Regexp)
+ }
+ return nil
+
+}
+
+type altAttrMatcher struct {
+ alts []attrMatcher
+}
+
+func compileAltAttrMatcher(cc compileContext, l []interface{}) (attrMatcher, error) {
+ alts := make([]attrMatcher, len(l))
+ for i, constraint := range l {
+ matcher1, err := compileAttrMatcher(cc.alt(i), constraint)
+ if err != nil {
+ return nil, err
+ }
+ alts[i] = matcher1
+ }
+ return altAttrMatcher{alts}, nil
+
+}
+
+func (matcher altAttrMatcher) match(context string, v interface{}) error {
+ var firstErr error
+ for _, alt := range matcher.alts {
+ err := alt.match(context, v)
+ if err == nil {
+ return nil
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ ctxDescr := ""
+ if context != "" {
+ ctxDescr = fmt.Sprintf(" for attribute %q", context)
+ }
+ return fmt.Errorf("no alternative%s matches: %v", ctxDescr, firstErr)
+}
+
+// AttributeConstraints implements a set of constraints on the attributes of a slot or plug.
+type AttributeConstraints struct {
+ matcher attrMatcher
+}
+
+// compileAttributeConstraints checks and compiles a mapping or list
+// from the assertion format into AttributeConstraints.
+func compileAttributeConstraints(constraints interface{}) (*AttributeConstraints, error) {
+ matcher, err := compileAttrMatcher(compileContext{}, constraints)
+ if err != nil {
+ return nil, err
+ }
+ return &AttributeConstraints{matcher: matcher}, nil
+}
+
+type fixedAttrMatcher struct {
+ result error
+}
+
+func (matcher fixedAttrMatcher) match(context string, v interface{}) error {
+ return matcher.result
+}
+
+var (
+ AlwaysMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{nil}}
+ NeverMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{errors.New("not allowed")}}
+)
+
+// Check checks whether attrs don't match the constraints.
+func (c *AttributeConstraints) Check(attrs map[string]interface{}) error {
+ return c.matcher.match("", attrs)
+}
+
+// OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets.
+type OnClassicConstraint struct {
+ Classic bool
+ SystemIDs []string
+}
+
+// rules
+
+var (
+ validSnapType = regexp.MustCompile("^(?:core|kernel|gadget|app)$")
+ validDistro = regexp.MustCompile("^[-0-9a-z._]+$")
+ validSnapID = regexp.MustCompile("^[a-z0-9A-Z]{32}$") // snap-ids look like this
+ validPublisher = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28}|\\$[A-Z][A-Z0-9_]*)$") // account ids look like snap-ids or are nice identifiers, support our own special markers $MARKER
+
+ validIDConstraints = map[string]*regexp.Regexp{
+ "slot-snap-type": validSnapType,
+ "slot-snap-id": validSnapID,
+ "slot-publisher-id": validPublisher,
+ "plug-snap-type": validSnapType,
+ "plug-snap-id": validSnapID,
+ "plug-publisher-id": validPublisher,
+ }
+)
+
+func checkMapOrShortcut(context string, v interface{}) (m map[string]interface{}, invert bool, err error) {
+ switch x := v.(type) {
+ case map[string]interface{}:
+ return x, false, nil
+ case string:
+ switch x {
+ case "true":
+ return nil, false, nil
+ case "false":
+ return nil, true, nil
+ }
+ }
+ return nil, false, errors.New("unexpected type")
+}
+
+type constraintsHolder interface {
+ setAttributeConstraints(field string, cstrs *AttributeConstraints)
+ setIDConstraints(field string, cstrs []string)
+ setOnClassicConstraint(onClassic *OnClassicConstraint)
+}
+
+func baseCompileConstraints(context string, cDef constraintsDef, target constraintsHolder, attrConstraints, idConstraints []string) error {
+ cMap := cDef.cMap
+ if cMap == nil {
+ fixed := AlwaysMatchAttributes // "true"
+ if cDef.invert { // "false"
+ fixed = NeverMatchAttributes
+ }
+ for _, field := range attrConstraints {
+ target.setAttributeConstraints(field, fixed)
+ }
+ return nil
+ }
+ defaultUsed := 0
+ for _, field := range idConstraints {
+ lst, err := checkStringListInMap(cMap, field, fmt.Sprintf("%s in %s", field, context), validIDConstraints[field])
+ if err != nil {
+ return err
+ }
+ if lst == nil {
+ defaultUsed++
+ }
+ target.setIDConstraints(field, lst)
+ }
+ for _, field := range attrConstraints {
+ cstrs := AlwaysMatchAttributes
+ v := cMap[field]
+ if v != nil {
+ var err error
+ cstrs, err = compileAttributeConstraints(cMap[field])
+ if err != nil {
+ return fmt.Errorf("cannot compile %s in %s: %v", field, context, err)
+ }
+ } else {
+ defaultUsed++
+ }
+ target.setAttributeConstraints(field, cstrs)
+ }
+ onClassic := cMap["on-classic"]
+ if onClassic == nil {
+ defaultUsed++
+ } else {
+ var c *OnClassicConstraint
+ switch x := onClassic.(type) {
+ case string:
+ switch x {
+ case "true":
+ c = &OnClassicConstraint{Classic: true}
+ case "false":
+ c = &OnClassicConstraint{Classic: false}
+ }
+ case []interface{}:
+ lst, err := checkStringListInMap(cMap, "on-classic", fmt.Sprintf("on-classic in %s", context), validDistro)
+ if err != nil {
+ return err
+ }
+ c = &OnClassicConstraint{Classic: true, SystemIDs: lst}
+ }
+ if c == nil {
+ return fmt.Errorf("on-classic in %s must be 'true', 'false' or a list of operating system IDs", context)
+ }
+ target.setOnClassicConstraint(c)
+ }
+ if defaultUsed == len(attributeConstraints)+len(idConstraints)+1 {
+ return fmt.Errorf("%s must specify at least one of %s, %s, on-classic", context, strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", "))
+ }
+ return nil
+}
+
+type rule interface {
+ setConstraints(field string, cstrs []constraintsHolder)
+}
+
+type constraintsDef struct {
+ cMap map[string]interface{}
+ invert bool
+}
+
+type subruleCompiler func(context string, def constraintsDef) (constraintsHolder, error)
+
+func baseCompileRule(context string, rule interface{}, target rule, subrules []string, compilers map[string]subruleCompiler, defaultOutcome, invertedOutcome map[string]interface{}) error {
+ rMap, invert, err := checkMapOrShortcut(context, rule)
+ if err != nil {
+ return fmt.Errorf("%s must be a map or one of the shortcuts 'true' or 'false'", context)
+ }
+ if rMap == nil {
+ rMap = defaultOutcome // "true"
+ if invert {
+ rMap = invertedOutcome // "false"
+ }
+ }
+ defaultUsed := 0
+ // compile and set subrules
+ for _, subrule := range subrules {
+ v := rMap[subrule]
+ var lst []interface{}
+ alternatives := false
+ switch x := v.(type) {
+ case nil:
+ v = defaultOutcome[subrule]
+ defaultUsed++
+ case []interface{}:
+ alternatives = true
+ lst = x
+ }
+ if lst == nil { // v is map or a string, checked below
+ lst = []interface{}{v}
+ }
+ compiler := compilers[subrule]
+ if compiler == nil {
+ panic(fmt.Sprintf("no compiler for %s in %s", subrule, context))
+ }
+ alts := make([]constraintsHolder, len(lst))
+ for i, alt := range lst {
+ subctxt := fmt.Sprintf("%s in %s", subrule, context)
+ if alternatives {
+ subctxt = fmt.Sprintf("alternative %d of %s", i+1, subctxt)
+ }
+ cMap, invert, err := checkMapOrShortcut(subctxt, alt)
+ if err != nil || (cMap == nil && alternatives) {
+ efmt := "%s must be a map"
+ if !alternatives {
+ efmt = "%s must be a map or one of the shortcuts 'true' or 'false'"
+ }
+ return fmt.Errorf(efmt, subctxt)
+ }
+
+ cstrs, err := compiler(subctxt, constraintsDef{
+ cMap: cMap,
+ invert: invert,
+ })
+ if err != nil {
+ return err
+ }
+ alts[i] = cstrs
+ }
+ target.setConstraints(subrule, alts)
+ }
+ if defaultUsed == len(subrules) {
+ return fmt.Errorf("%s must specify at least one of %s", context, strings.Join(subrules, ", "))
+ }
+ return nil
+}
+
+// PlugRule holds the rule of what is allowed, wrt installation and
+// connection, for a plug of a specific interface for a snap.
+type PlugRule struct {
+ Interface string
+
+ AllowInstallation []*PlugInstallationConstraints
+ DenyInstallation []*PlugInstallationConstraints
+
+ AllowConnection []*PlugConnectionConstraints
+ DenyConnection []*PlugConnectionConstraints
+
+ AllowAutoConnection []*PlugConnectionConstraints
+ DenyAutoConnection []*PlugConnectionConstraints
+}
+
+func castPlugInstallationConstraints(cstrs []constraintsHolder) (res []*PlugInstallationConstraints) {
+ res = make([]*PlugInstallationConstraints, len(cstrs))
+ for i, cstr := range cstrs {
+ res[i] = cstr.(*PlugInstallationConstraints)
+ }
+ return res
+}
+
+func castPlugConnectionConstraints(cstrs []constraintsHolder) (res []*PlugConnectionConstraints) {
+ res = make([]*PlugConnectionConstraints, len(cstrs))
+ for i, cstr := range cstrs {
+ res[i] = cstr.(*PlugConnectionConstraints)
+ }
+ return res
+}
+
+func (r *PlugRule) setConstraints(field string, cstrs []constraintsHolder) {
+ if len(cstrs) == 0 {
+ panic(fmt.Sprintf("cannot set PlugRule field %q to empty", field))
+ }
+ switch cstrs[0].(type) {
+ case *PlugInstallationConstraints:
+ switch field {
+ case "allow-installation":
+ r.AllowInstallation = castPlugInstallationConstraints(cstrs)
+ return
+ case "deny-installation":
+ r.DenyInstallation = castPlugInstallationConstraints(cstrs)
+ return
+ }
+ case *PlugConnectionConstraints:
+ switch field {
+ case "allow-connection":
+ r.AllowConnection = castPlugConnectionConstraints(cstrs)
+ return
+ case "deny-connection":
+ r.DenyConnection = castPlugConnectionConstraints(cstrs)
+ return
+ case "allow-auto-connection":
+ r.AllowAutoConnection = castPlugConnectionConstraints(cstrs)
+ return
+ case "deny-auto-connection":
+ r.DenyAutoConnection = castPlugConnectionConstraints(cstrs)
+ return
+ }
+ }
+ panic(fmt.Sprintf("cannot set PlugRule field %q with %T elements", field, cstrs[0]))
+}
+
+// PlugInstallationConstraints specifies a set of constraints on an interface plug relevant to the installation of snap.
+type PlugInstallationConstraints struct {
+ PlugSnapTypes []string
+
+ PlugAttributes *AttributeConstraints
+
+ OnClassic *OnClassicConstraint
+}
+
+func (c *PlugInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) {
+ switch field {
+ case "plug-attributes":
+ c.PlugAttributes = cstrs
+ default:
+ panic("unknown PlugInstallationConstraints field " + field)
+ }
+}
+
+func (c *PlugInstallationConstraints) setIDConstraints(field string, cstrs []string) {
+ switch field {
+ case "plug-snap-type":
+ c.PlugSnapTypes = cstrs
+ default:
+ panic("unknown PlugInstallationConstraints field " + field)
+ }
+}
+
+func (c *PlugInstallationConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) {
+ c.OnClassic = onClassic
+}
+
+func compilePlugInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) {
+ plugInstCstrs := &PlugInstallationConstraints{}
+ err := baseCompileConstraints(context, cDef, plugInstCstrs, []string{"plug-attributes"}, []string{"plug-snap-type"})
+ if err != nil {
+ return nil, err
+ }
+ return plugInstCstrs, nil
+}
+
+// PlugConnectionConstraints specfies a set of constraints on an
+// interface plug for a snap relevant to its connection or
+// auto-connection.
+type PlugConnectionConstraints struct {
+ SlotSnapTypes []string
+ SlotSnapIDs []string
+ SlotPublisherIDs []string
+
+ PlugAttributes *AttributeConstraints
+ SlotAttributes *AttributeConstraints
+
+ OnClassic *OnClassicConstraint
+}
+
+func (c *PlugConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) {
+ switch field {
+ case "plug-attributes":
+ c.PlugAttributes = cstrs
+ case "slot-attributes":
+ c.SlotAttributes = cstrs
+ default:
+ panic("unknown PlugConnectionConstraints field " + field)
+ }
+}
+
+func (c *PlugConnectionConstraints) setIDConstraints(field string, cstrs []string) {
+ switch field {
+ case "slot-snap-type":
+ c.SlotSnapTypes = cstrs
+ case "slot-snap-id":
+ c.SlotSnapIDs = cstrs
+ case "slot-publisher-id":
+ c.SlotPublisherIDs = cstrs
+ default:
+ panic("unknown PlugConnectionConstraints field " + field)
+ }
+}
+
+func (c *PlugConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) {
+ c.OnClassic = onClassic
+}
+
+var (
+ attributeConstraints = []string{"plug-attributes", "slot-attributes"}
+ plugIDConstraints = []string{"slot-snap-type", "slot-publisher-id", "slot-snap-id"}
+)
+
+func compilePlugConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) {
+ plugConnCstrs := &PlugConnectionConstraints{}
+ err := baseCompileConstraints(context, cDef, plugConnCstrs, attributeConstraints, plugIDConstraints)
+ if err != nil {
+ return nil, err
+ }
+ return plugConnCstrs, nil
+}
+
+var (
+ defaultOutcome = map[string]interface{}{
+ "allow-installation": "true",
+ "allow-connection": "true",
+ "allow-auto-connection": "true",
+ "deny-installation": "false",
+ "deny-connection": "false",
+ "deny-auto-connection": "false",
+ }
+
+ invertedOutcome = map[string]interface{}{
+ "allow-installation": "false",
+ "allow-connection": "false",
+ "allow-auto-connection": "false",
+ "deny-installation": "true",
+ "deny-connection": "true",
+ "deny-auto-connection": "true",
+ }
+
+ ruleSubrules = []string{"allow-installation", "deny-installation", "allow-connection", "deny-connection", "allow-auto-connection", "deny-auto-connection"}
+)
+
+var plugRuleCompilers = map[string]subruleCompiler{
+ "allow-installation": compilePlugInstallationConstraints,
+ "deny-installation": compilePlugInstallationConstraints,
+ "allow-connection": compilePlugConnectionConstraints,
+ "deny-connection": compilePlugConnectionConstraints,
+ "allow-auto-connection": compilePlugConnectionConstraints,
+ "deny-auto-connection": compilePlugConnectionConstraints,
+}
+
+func compilePlugRule(interfaceName string, rule interface{}) (*PlugRule, error) {
+ context := fmt.Sprintf("plug rule for interface %q", interfaceName)
+ plugRule := &PlugRule{
+ Interface: interfaceName,
+ }
+ err := baseCompileRule(context, rule, plugRule, ruleSubrules, plugRuleCompilers, defaultOutcome, invertedOutcome)
+ if err != nil {
+ return nil, err
+ }
+ return plugRule, nil
+}
+
+// SlotRule holds the rule of what is allowed, wrt installation and
+// connection, for a slot of a specific interface for a snap.
+type SlotRule struct {
+ Interface string
+
+ AllowInstallation []*SlotInstallationConstraints
+ DenyInstallation []*SlotInstallationConstraints
+
+ AllowConnection []*SlotConnectionConstraints
+ DenyConnection []*SlotConnectionConstraints
+
+ AllowAutoConnection []*SlotConnectionConstraints
+ DenyAutoConnection []*SlotConnectionConstraints
+}
+
+func castSlotInstallationConstraints(cstrs []constraintsHolder) (res []*SlotInstallationConstraints) {
+ res = make([]*SlotInstallationConstraints, len(cstrs))
+ for i, cstr := range cstrs {
+ res[i] = cstr.(*SlotInstallationConstraints)
+ }
+ return res
+}
+
+func castSlotConnectionConstraints(cstrs []constraintsHolder) (res []*SlotConnectionConstraints) {
+ res = make([]*SlotConnectionConstraints, len(cstrs))
+ for i, cstr := range cstrs {
+ res[i] = cstr.(*SlotConnectionConstraints)
+ }
+ return res
+}
+
+func (r *SlotRule) setConstraints(field string, cstrs []constraintsHolder) {
+ if len(cstrs) == 0 {
+ panic(fmt.Sprintf("cannot set SlotRule field %q to empty", field))
+ }
+ switch cstrs[0].(type) {
+ case *SlotInstallationConstraints:
+ switch field {
+ case "allow-installation":
+ r.AllowInstallation = castSlotInstallationConstraints(cstrs)
+ return
+ case "deny-installation":
+ r.DenyInstallation = castSlotInstallationConstraints(cstrs)
+ return
+ }
+ case *SlotConnectionConstraints:
+ switch field {
+ case "allow-connection":
+ r.AllowConnection = castSlotConnectionConstraints(cstrs)
+ return
+ case "deny-connection":
+ r.DenyConnection = castSlotConnectionConstraints(cstrs)
+ return
+ case "allow-auto-connection":
+ r.AllowAutoConnection = castSlotConnectionConstraints(cstrs)
+ return
+ case "deny-auto-connection":
+ r.DenyAutoConnection = castSlotConnectionConstraints(cstrs)
+ return
+ }
+ }
+ panic(fmt.Sprintf("cannot set SlotRule field %q with %T elements", field, cstrs[0]))
+}
+
+// SlotInstallationConstraints specifies a set of constraints on an
+// interface slot relevant to the installation of snap.
+type SlotInstallationConstraints struct {
+ SlotSnapTypes []string
+
+ SlotAttributes *AttributeConstraints
+
+ OnClassic *OnClassicConstraint
+}
+
+func (c *SlotInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) {
+ switch field {
+ case "slot-attributes":
+ c.SlotAttributes = cstrs
+ default:
+ panic("unknown SlotInstallationConstraints field " + field)
+ }
+}
+
+func (c *SlotInstallationConstraints) setIDConstraints(field string, cstrs []string) {
+ switch field {
+ case "slot-snap-type":
+ c.SlotSnapTypes = cstrs
+ default:
+ panic("unknown SlotInstallationConstraints field " + field)
+ }
+}
+
+func (c *SlotInstallationConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) {
+ c.OnClassic = onClassic
+}
+
+func compileSlotInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) {
+ slotInstCstrs := &SlotInstallationConstraints{}
+ err := baseCompileConstraints(context, cDef, slotInstCstrs, []string{"slot-attributes"}, []string{"slot-snap-type"})
+ if err != nil {
+ return nil, err
+ }
+ return slotInstCstrs, nil
+}
+
+// SlotConnectionConstraints specfies a set of constraints on an
+// interface slot for a snap relevant to its connection or
+// auto-connection.
+type SlotConnectionConstraints struct {
+ PlugSnapTypes []string
+ PlugSnapIDs []string
+ PlugPublisherIDs []string
+
+ SlotAttributes *AttributeConstraints
+ PlugAttributes *AttributeConstraints
+
+ OnClassic *OnClassicConstraint
+}
+
+func (c *SlotConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) {
+ switch field {
+ case "plug-attributes":
+ c.PlugAttributes = cstrs
+ case "slot-attributes":
+ c.SlotAttributes = cstrs
+ default:
+ panic("unknown SlotConnectionConstraints field " + field)
+ }
+}
+
+func (c *SlotConnectionConstraints) setIDConstraints(field string, cstrs []string) {
+ switch field {
+ case "plug-snap-type":
+ c.PlugSnapTypes = cstrs
+ case "plug-snap-id":
+ c.PlugSnapIDs = cstrs
+ case "plug-publisher-id":
+ c.PlugPublisherIDs = cstrs
+ default:
+ panic("unknown SlotConnectionConstraints field " + field)
+ }
+}
+
+var (
+ slotIDConstraints = []string{"plug-snap-type", "plug-publisher-id", "plug-snap-id"}
+)
+
+func (c *SlotConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) {
+ c.OnClassic = onClassic
+}
+
+func compileSlotConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) {
+ slotConnCstrs := &SlotConnectionConstraints{}
+ err := baseCompileConstraints(context, cDef, slotConnCstrs, attributeConstraints, slotIDConstraints)
+ if err != nil {
+ return nil, err
+ }
+ return slotConnCstrs, nil
+}
+
+var slotRuleCompilers = map[string]subruleCompiler{
+ "allow-installation": compileSlotInstallationConstraints,
+ "deny-installation": compileSlotInstallationConstraints,
+ "allow-connection": compileSlotConnectionConstraints,
+ "deny-connection": compileSlotConnectionConstraints,
+ "allow-auto-connection": compileSlotConnectionConstraints,
+ "deny-auto-connection": compileSlotConnectionConstraints,
+}
+
+func compileSlotRule(interfaceName string, rule interface{}) (*SlotRule, error) {
+ context := fmt.Sprintf("slot rule for interface %q", interfaceName)
+ slotRule := &SlotRule{
+ Interface: interfaceName,
+ }
+ err := baseCompileRule(context, rule, slotRule, ruleSubrules, slotRuleCompilers, defaultOutcome, invertedOutcome)
+ if err != nil {
+ return nil, err
+ }
+ return slotRule, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/snap"
+)
+
+var (
+ _ = Suite(&attrConstraintsSuite{})
+ _ = Suite(&plugSlotRulesSuite{})
+)
+
+type attrConstraintsSuite struct{}
+
+func attrs(yml string) map[string]interface{} {
+ var attrs map[string]interface{}
+ err := yaml.Unmarshal([]byte(yml), &attrs)
+ if err != nil {
+ panic(err)
+ }
+ snapYaml, err := yaml.Marshal(map[string]interface{}{
+ "name": "sample",
+ "plugs": map[string]interface{}{
+ "plug": attrs,
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+ info, err := snap.InfoFromSnapYaml(snapYaml)
+ if err != nil {
+ panic(err)
+ }
+ return info.Plugs["plug"].Attrs
+}
+
+func (s *attrConstraintsSuite) TestSimple(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ foo: FOO
+ bar: BAR`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": "FOO",
+ "bar": "BAR",
+ "baz": "BAZ",
+ })
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": "FOO",
+ "bar": "BAZ",
+ "baz": "BAZ",
+ })
+ c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": "FOO",
+ "baz": "BAZ",
+ })
+ c.Check(err, ErrorMatches, `attribute "bar" has constraints but is unset`)
+}
+
+func (s *attrConstraintsSuite) TestSimpleAnchorsVsRegexpAlt(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ bar: BAR|BAZ`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "bar": "BAR",
+ })
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "bar": "BARR",
+ })
+ c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`)
+
+ err = cstrs.Check(map[string]interface{}{
+ "bar": "BBAZ",
+ })
+ c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`)
+
+ err = cstrs.Check(map[string]interface{}{
+ "bar": "BABAZ",
+ })
+ c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`)
+
+ err = cstrs.Check(map[string]interface{}{
+ "bar": "BARAZ",
+ })
+ c.Check(err, ErrorMatches, `attribute "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`)
+}
+
+func (s *attrConstraintsSuite) TestNested(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ foo: FOO
+ bar:
+ bar1: BAR1
+ bar2: BAR2`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar:
+ bar1: BAR1
+ bar2: BAR2
+ bar3: BAR3
+baz: BAZ
+`))
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar: BAZ
+baz: BAZ
+`))
+ c.Check(err, ErrorMatches, `attribute "bar" must be a map`)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar:
+ bar1: BAR1
+ bar2: BAR22
+ bar3: BAR3
+baz: BAZ
+`))
+ c.Check(err, ErrorMatches, `attribute "bar\.bar2" value "BAR22" does not match \^\(BAR2\)\$`)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar:
+ bar1: BAR1
+ bar2:
+ bar22: true
+ bar3: BAR3
+baz: BAZ
+`))
+ c.Check(err, ErrorMatches, `attribute "bar\.bar2" must be a scalar or list`)
+}
+
+func (s *attrConstraintsSuite) TestAlternative(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ -
+ foo: FOO
+ bar: BAR
+ -
+ foo: FOO
+ bar: BAZ`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].([]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": "FOO",
+ "bar": "BAR",
+ "baz": "BAZ",
+ })
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": "FOO",
+ "bar": "BAZ",
+ "baz": "BAZ",
+ })
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": "FOO",
+ "bar": "BARR",
+ "baz": "BAR",
+ })
+ c.Check(err, ErrorMatches, `no alternative matches: attribute "bar" value "BARR" does not match \^\(BAR\)\$`)
+}
+
+func (s *attrConstraintsSuite) TestNestedAlternative(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ foo: FOO
+ bar:
+ bar1: BAR1
+ bar2:
+ - BAR2
+ - BAR22`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar:
+ bar1: BAR1
+ bar2: BAR2
+`))
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar:
+ bar1: BAR1
+ bar2: BAR22
+`))
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: FOO
+bar:
+ bar1: BAR1
+ bar2: BAR3
+`))
+ c.Check(err, ErrorMatches, `no alternative for attribute "bar\.bar2" matches: attribute "bar\.bar2" value "BAR3" does not match \^\(BAR2\)\$`)
+}
+
+func (s *attrConstraintsSuite) TestOtherScalars(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ foo: 1
+ bar: true`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: 1
+bar: true
+`))
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(map[string]interface{}{
+ "foo": int64(1),
+ "bar": true,
+ })
+ c.Check(err, IsNil)
+}
+
+func (s *attrConstraintsSuite) TestCompileErrors(c *C) {
+ _, err := asserts.CompileAttributeConstraints(map[string]interface{}{
+ "foo": "[",
+ })
+ c.Check(err, ErrorMatches, `cannot compile "foo" constraint "\[": error parsing regexp:.*`)
+
+ _, err = asserts.CompileAttributeConstraints(map[string]interface{}{
+ "foo": []interface{}{"foo", "["},
+ })
+ c.Check(err, ErrorMatches, `cannot compile "foo/alt#2/" constraint "\[": error parsing regexp:.*`)
+
+ _, err = asserts.CompileAttributeConstraints(map[string]interface{}{
+ "foo": []interface{}{"foo", []interface{}{"bar", "baz"}},
+ })
+ c.Check(err, ErrorMatches, `cannot nest alternative constraints directly at "foo/alt#2/"`)
+
+ _, err = asserts.CompileAttributeConstraints("FOO")
+ c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`)
+
+ _, err = asserts.CompileAttributeConstraints([]interface{}{"FOO"})
+ c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`)
+}
+
+func (s *attrConstraintsSuite) TestMatchingListsSimple(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ foo: /foo/.*`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: ["/foo/x", "/foo/y"]
+`))
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: ["/foo/x", "/foo"]
+`))
+ c.Check(err, ErrorMatches, `attribute "foo\.1" value "/foo" does not match \^\(/foo/\.\*\)\$`)
+}
+
+func (s *attrConstraintsSuite) TestMatchingListsMap(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`attrs:
+ foo:
+ p: /foo/.*`))
+ c.Assert(err, IsNil)
+
+ cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: [{p: "/foo/x"}, {p: "/foo/y"}]
+`))
+ c.Check(err, IsNil)
+
+ err = cstrs.Check(attrs(`
+foo: [{p: "zzz"}, {p: "/foo/y"}]
+`))
+ c.Check(err, ErrorMatches, `attribute "foo\.0\.p" value "zzz" does not match \^\(/foo/\.\*\)\$`)
+}
+
+func (s *attrConstraintsSuite) TestAlwaysMatchAttributeConstraints(c *C) {
+ c.Check(asserts.AlwaysMatchAttributes.Check(nil), IsNil)
+}
+
+func (s *attrConstraintsSuite) TestNeverMatchAttributeConstraints(c *C) {
+ c.Check(asserts.NeverMatchAttributes.Check(nil), NotNil)
+}
+
+type plugSlotRulesSuite struct{}
+
+func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) {
+ c.Check(attrs.Check(map[string]interface{}{
+ witness: "XYZ",
+ }), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness))
+ c.Check(attrs.Check(map[string]interface{}{
+ witness: expected,
+ }), IsNil)
+}
+
+func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) {
+ expected := asserts.NeverMatchAttributes
+ if always {
+ expected = asserts.AlwaysMatchAttributes
+ }
+ c.Assert(cstrs, HasLen, 1)
+ cstrs1 := cstrs[0]
+ c.Check(cstrs1.PlugAttributes, Equals, expected)
+ c.Check(cstrs1.SlotAttributes, Equals, expected)
+ c.Check(cstrs1.SlotSnapIDs, HasLen, 0)
+ c.Check(cstrs1.SlotPublisherIDs, HasLen, 0)
+ c.Check(cstrs1.SlotSnapTypes, HasLen, 0)
+}
+
+func checkBoolSlotConnConstraints(c *C, cstrs []*asserts.SlotConnectionConstraints, always bool) {
+ expected := asserts.NeverMatchAttributes
+ if always {
+ expected = asserts.AlwaysMatchAttributes
+ }
+ c.Assert(cstrs, HasLen, 1)
+ cstrs1 := cstrs[0]
+ c.Check(cstrs1.PlugAttributes, Equals, expected)
+ c.Check(cstrs1.SlotAttributes, Equals, expected)
+ c.Check(cstrs1.PlugSnapIDs, HasLen, 0)
+ c.Check(cstrs1.PlugPublisherIDs, HasLen, 0)
+ c.Check(cstrs1.PlugSnapTypes, HasLen, 0)
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleAllAllowDenyStanzas(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ plug-attributes:
+ a1: A1
+ deny-installation:
+ plug-attributes:
+ a2: A2
+ allow-connection:
+ plug-attributes:
+ pa3: PA3
+ slot-attributes:
+ sa3: SA3
+ deny-connection:
+ plug-attributes:
+ pa4: PA4
+ slot-attributes:
+ sa4: SA4
+ allow-auto-connection:
+ plug-attributes:
+ pa5: PA5
+ slot-attributes:
+ sa5: SA5
+ deny-auto-connection:
+ plug-attributes:
+ pa6: PA6
+ slot-attributes:
+ sa6: SA6`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.Interface, Equals, "iface")
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ checkAttrs(c, rule.AllowInstallation[0].PlugAttributes, "a1", "A1")
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ checkAttrs(c, rule.DenyInstallation[0].PlugAttributes, "a2", "A2")
+ // connection subrules
+ c.Assert(rule.AllowConnection, HasLen, 1)
+ checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3")
+ checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3")
+ c.Assert(rule.DenyConnection, HasLen, 1)
+ checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4")
+ checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4")
+ // auto-connection subrules
+ c.Assert(rule.AllowAutoConnection, HasLen, 1)
+ checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5")
+ checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5")
+ c.Assert(rule.DenyAutoConnection, HasLen, 1)
+ checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6")
+ checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6")
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleAllAllowDenyOrStanzas(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ -
+ plug-attributes:
+ a1: A1
+ -
+ plug-attributes:
+ a1: A1alt
+ deny-installation:
+ -
+ plug-attributes:
+ a2: A2
+ -
+ plug-attributes:
+ a2: A2alt
+ allow-connection:
+ -
+ plug-attributes:
+ pa3: PA3
+ slot-attributes:
+ sa3: SA3
+ -
+ plug-attributes:
+ pa3: PA3alt
+ deny-connection:
+ -
+ plug-attributes:
+ pa4: PA4
+ slot-attributes:
+ sa4: SA4
+ -
+ plug-attributes:
+ pa4: PA4alt
+ allow-auto-connection:
+ -
+ plug-attributes:
+ pa5: PA5
+ slot-attributes:
+ sa5: SA5
+ -
+ plug-attributes:
+ pa5: PA5alt
+ deny-auto-connection:
+ -
+ plug-attributes:
+ pa6: PA6
+ slot-attributes:
+ sa6: SA6
+ -
+ plug-attributes:
+ pa6: PA6alt`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.Interface, Equals, "iface")
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 2)
+ checkAttrs(c, rule.AllowInstallation[0].PlugAttributes, "a1", "A1")
+ checkAttrs(c, rule.AllowInstallation[1].PlugAttributes, "a1", "A1alt")
+ c.Assert(rule.DenyInstallation, HasLen, 2)
+ checkAttrs(c, rule.DenyInstallation[0].PlugAttributes, "a2", "A2")
+ checkAttrs(c, rule.DenyInstallation[1].PlugAttributes, "a2", "A2alt")
+ // connection subrules
+ c.Assert(rule.AllowConnection, HasLen, 2)
+ checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3")
+ checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3")
+ checkAttrs(c, rule.AllowConnection[1].PlugAttributes, "pa3", "PA3alt")
+ c.Assert(rule.DenyConnection, HasLen, 2)
+ checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4")
+ checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4")
+ checkAttrs(c, rule.DenyConnection[1].PlugAttributes, "pa4", "PA4alt")
+ // auto-connection subrules
+ c.Assert(rule.AllowAutoConnection, HasLen, 2)
+ checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5")
+ checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5")
+ checkAttrs(c, rule.AllowAutoConnection[1].PlugAttributes, "pa5", "PA5alt")
+ c.Assert(rule.DenyAutoConnection, HasLen, 2)
+ checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6")
+ checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6")
+ checkAttrs(c, rule.DenyAutoConnection[1].PlugAttributes, "pa6", "PA6alt")
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutTrue(c *C) {
+ rule, err := asserts.CompilePlugRule("iface", "true")
+ c.Assert(err, IsNil)
+
+ c.Check(rule.Interface, Equals, "iface")
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes)
+ // connection subrules
+ checkBoolPlugConnConstraints(c, rule.AllowConnection, true)
+ checkBoolPlugConnConstraints(c, rule.DenyConnection, false)
+ // auto-connection subrules
+ checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true)
+ checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, false)
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutFalse(c *C) {
+ rule, err := asserts.CompilePlugRule("iface", "false")
+ c.Assert(err, IsNil)
+
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes)
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes)
+ // connection subrules
+ checkBoolPlugConnConstraints(c, rule.AllowConnection, false)
+ checkBoolPlugConnConstraints(c, rule.DenyConnection, true)
+ // auto-connection subrules
+ checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, false)
+ checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true)
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleDefaults(c *C) {
+ rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{
+ "deny-auto-connection": "true",
+ })
+ c.Assert(err, IsNil)
+
+ // everything follows the defaults...
+
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes)
+ // connection subrules
+ checkBoolPlugConnConstraints(c, rule.AllowConnection, true)
+ checkBoolPlugConnConstraints(c, rule.DenyConnection, false)
+ // auto-connection subrules
+ checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true)
+ // ... but deny-auto-connection is on
+ checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true)
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleInstalationConstraintsIDConstraints(c *C) {
+ rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{
+ "allow-installation": map[string]interface{}{
+ "plug-snap-type": []interface{}{"core", "kernel", "gadget", "app"},
+ },
+ })
+ c.Assert(err, IsNil)
+
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ cstrs := rule.AllowInstallation[0]
+ c.Check(cstrs.PlugSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"})
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleInstallationConstraintsOnClassic(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-installation: true`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, IsNil)
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ on-classic: false`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ on-classic: true`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ on-classic:
+ - ubuntu
+ - debian`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}})
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsIDConstraints(c *C) {
+ rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{
+ "allow-connection": map[string]interface{}{
+ "slot-snap-type": []interface{}{"core", "kernel", "gadget", "app"},
+ "slot-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"},
+ "slot-publisher-id": []interface{}{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"},
+ },
+ })
+ c.Assert(err, IsNil)
+
+ c.Assert(rule.AllowConnection, HasLen, 1)
+ cstrs := rule.AllowConnection[0]
+ c.Check(cstrs.SlotSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"})
+ c.Check(cstrs.SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"})
+ c.Check(cstrs.SlotPublisherIDs, DeepEquals, []string{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"})
+
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsOnClassic(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-connection: true`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, IsNil)
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-connection:
+ on-classic: false`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-connection:
+ on-classic: true`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-connection:
+ on-classic:
+ - ubuntu
+ - debian`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}})
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsAttributesDefault(c *C) {
+ rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{
+ "allow-connection": map[string]interface{}{
+ "slot-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01"},
+ },
+ })
+ c.Assert(err, IsNil)
+
+ // attributes default to always matching here
+ cstrs := rule.AllowConnection[0]
+ c.Check(cstrs.PlugAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Check(cstrs.SlotAttributes, Equals, asserts.AlwaysMatchAttributes)
+}
+
+func (s *plugSlotRulesSuite) TestCompilePlugRuleErrors(c *C) {
+ tests := []struct {
+ stanza string
+ err string
+ }{
+ {`iface: foo`, `plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ - allow`, `plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ allow-installation: foo`, `allow-installation in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ deny-installation: foo`, `deny-installation in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ allow-connection: foo`, `allow-connection in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ allow-connection:
+ - foo`, `alternative 1 of allow-connection in plug rule for interface "iface" must be a map`},
+ {`iface:
+ allow-connection:
+ - true`, `alternative 1 of allow-connection in plug rule for interface "iface" must be a map`},
+ {`iface:
+ allow-installation:
+ plug-attributes:
+ a1: [`, `cannot compile plug-attributes in allow-installation in plug rule for interface "iface": cannot compile "a1" constraint .*`},
+ {`iface:
+ allow-connection:
+ slot-attributes:
+ a2: [`, `cannot compile slot-attributes in allow-connection in plug rule for interface "iface": cannot compile "a2" constraint .*`},
+ {`iface:
+ allow-connection:
+ slot-snap-id:
+ -
+ foo: 1`, `slot-snap-id in allow-connection in plug rule for interface "iface" must be a list of strings`},
+ {`iface:
+ allow-connection:
+ slot-snap-id:
+ - foo`, `slot-snap-id in allow-connection in plug rule for interface "iface" contains an invalid element: "foo"`},
+ {`iface:
+ allow-connection:
+ slot-snap-type:
+ - foo`, `slot-snap-type in allow-connection in plug rule for interface "iface" contains an invalid element: "foo"`},
+ {`iface:
+ allow-connection:
+ slot-snap-type:
+ - xapp`, `slot-snap-type in allow-connection in plug rule for interface "iface" contains an invalid element: "xapp"`},
+ {`iface:
+ allow-connection:
+ slot-snap-ids:
+ - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`},
+ {`iface:
+ deny-connection:
+ slot-snap-ids:
+ - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`},
+ {`iface:
+ allow-auto-connection:
+ slot-snap-ids:
+ - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`},
+ {`iface:
+ deny-auto-connection:
+ slot-snap-ids:
+ - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`},
+ {`iface:
+ allow-connect: true`, `plug rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`},
+ }
+
+ for _, t := range tests {
+ m, err := asserts.ParseHeaders([]byte(t.stanza))
+ c.Assert(err, IsNil, Commentf(t.stanza))
+
+ _, err = asserts.CompilePlugRule("iface", m["iface"])
+ c.Check(err, ErrorMatches, t.err, Commentf(t.stanza))
+ }
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyStanzas(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ slot-attributes:
+ a1: A1
+ deny-installation:
+ slot-attributes:
+ a2: A2
+ allow-connection:
+ plug-attributes:
+ pa3: PA3
+ slot-attributes:
+ sa3: SA3
+ deny-connection:
+ plug-attributes:
+ pa4: PA4
+ slot-attributes:
+ sa4: SA4
+ allow-auto-connection:
+ plug-attributes:
+ pa5: PA5
+ slot-attributes:
+ sa5: SA5
+ deny-auto-connection:
+ plug-attributes:
+ pa6: PA6
+ slot-attributes:
+ sa6: SA6`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.Interface, Equals, "iface")
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ checkAttrs(c, rule.AllowInstallation[0].SlotAttributes, "a1", "A1")
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ checkAttrs(c, rule.DenyInstallation[0].SlotAttributes, "a2", "A2")
+ // connection subrules
+ c.Assert(rule.AllowConnection, HasLen, 1)
+ checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3")
+ checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3")
+ c.Assert(rule.DenyConnection, HasLen, 1)
+ checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4")
+ checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4")
+ // auto-connection subrules
+ c.Assert(rule.AllowAutoConnection, HasLen, 1)
+ checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5")
+ checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5")
+ c.Assert(rule.DenyAutoConnection, HasLen, 1)
+ checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6")
+ checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6")
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyOrStanzas(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ -
+ slot-attributes:
+ a1: A1
+ -
+ slot-attributes:
+ a1: A1alt
+ deny-installation:
+ -
+ slot-attributes:
+ a2: A2
+ -
+ slot-attributes:
+ a2: A2alt
+ allow-connection:
+ -
+ plug-attributes:
+ pa3: PA3
+ slot-attributes:
+ sa3: SA3
+ -
+ slot-attributes:
+ sa3: SA3alt
+ deny-connection:
+ -
+ plug-attributes:
+ pa4: PA4
+ slot-attributes:
+ sa4: SA4
+ -
+ slot-attributes:
+ sa4: SA4alt
+ allow-auto-connection:
+ -
+ plug-attributes:
+ pa5: PA5
+ slot-attributes:
+ sa5: SA5
+ -
+ slot-attributes:
+ sa5: SA5alt
+ deny-auto-connection:
+ -
+ plug-attributes:
+ pa6: PA6
+ slot-attributes:
+ sa6: SA6
+ -
+ slot-attributes:
+ sa6: SA6alt`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.Interface, Equals, "iface")
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 2)
+ checkAttrs(c, rule.AllowInstallation[0].SlotAttributes, "a1", "A1")
+ checkAttrs(c, rule.AllowInstallation[1].SlotAttributes, "a1", "A1alt")
+ c.Assert(rule.DenyInstallation, HasLen, 2)
+ checkAttrs(c, rule.DenyInstallation[0].SlotAttributes, "a2", "A2")
+ checkAttrs(c, rule.DenyInstallation[1].SlotAttributes, "a2", "A2alt")
+ // connection subrules
+ c.Assert(rule.AllowConnection, HasLen, 2)
+ checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3")
+ checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3")
+ checkAttrs(c, rule.AllowConnection[1].SlotAttributes, "sa3", "SA3alt")
+ c.Assert(rule.DenyConnection, HasLen, 2)
+ checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4")
+ checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4")
+ checkAttrs(c, rule.DenyConnection[1].SlotAttributes, "sa4", "SA4alt")
+ // auto-connection subrules
+ c.Assert(rule.AllowAutoConnection, HasLen, 2)
+ checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5")
+ checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5")
+ checkAttrs(c, rule.AllowAutoConnection[1].SlotAttributes, "sa5", "SA5alt")
+ c.Assert(rule.DenyAutoConnection, HasLen, 2)
+ checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6")
+ checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6")
+ checkAttrs(c, rule.DenyAutoConnection[1].SlotAttributes, "sa6", "SA6alt")
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutTrue(c *C) {
+ rule, err := asserts.CompileSlotRule("iface", "true")
+ c.Assert(err, IsNil)
+
+ c.Check(rule.Interface, Equals, "iface")
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes)
+ // connection subrules
+ checkBoolSlotConnConstraints(c, rule.AllowConnection, true)
+ checkBoolSlotConnConstraints(c, rule.DenyConnection, false)
+ // auto-connection subrules
+ checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true)
+ checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, false)
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutFalse(c *C) {
+ rule, err := asserts.CompileSlotRule("iface", "false")
+ c.Assert(err, IsNil)
+
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes)
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes)
+ // connection subrules
+ checkBoolSlotConnConstraints(c, rule.AllowConnection, false)
+ checkBoolSlotConnConstraints(c, rule.DenyConnection, true)
+ // auto-connection subrules
+ checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, false)
+ checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true)
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleDefaults(c *C) {
+ rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{
+ "deny-auto-connection": "true",
+ })
+ c.Assert(err, IsNil)
+
+ // everything follows the defaults...
+
+ // install subrules
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Assert(rule.DenyInstallation, HasLen, 1)
+ c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes)
+ // connection subrules
+ checkBoolSlotConnConstraints(c, rule.AllowConnection, true)
+ checkBoolSlotConnConstraints(c, rule.DenyConnection, false)
+ // auto-connection subrules
+ checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true)
+ // ... but deny-auto-connection is on
+ checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true)
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsIDConstraints(c *C) {
+ rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{
+ "allow-installation": map[string]interface{}{
+ "slot-snap-type": []interface{}{"core", "kernel", "gadget", "app"},
+ },
+ })
+ c.Assert(err, IsNil)
+
+ c.Assert(rule.AllowInstallation, HasLen, 1)
+ cstrs := rule.AllowInstallation[0]
+ c.Check(cstrs.SlotSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"})
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsOnClassic(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-installation: true`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, IsNil)
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ on-classic: false`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ on-classic: true`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-installation:
+ on-classic:
+ - ubuntu
+ - debian`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}})
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsIDConstraints(c *C) {
+ rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{
+ "allow-connection": map[string]interface{}{
+ "plug-snap-type": []interface{}{"core", "kernel", "gadget", "app"},
+ "plug-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"},
+ "plug-publisher-id": []interface{}{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"},
+ },
+ })
+ c.Assert(err, IsNil)
+
+ c.Assert(rule.AllowConnection, HasLen, 1)
+ cstrs := rule.AllowConnection[0]
+ c.Check(cstrs.PlugSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"})
+ c.Check(cstrs.PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"})
+ c.Check(cstrs.PlugPublisherIDs, DeepEquals, []string{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"})
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsOnClassic(c *C) {
+ m, err := asserts.ParseHeaders([]byte(`iface:
+ allow-connection: true`))
+ c.Assert(err, IsNil)
+
+ rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, IsNil)
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-connection:
+ on-classic: false`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-connection:
+ on-classic: true`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true})
+
+ m, err = asserts.ParseHeaders([]byte(`iface:
+ allow-connection:
+ on-classic:
+ - ubuntu
+ - debian`))
+ c.Assert(err, IsNil)
+
+ rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
+ c.Assert(err, IsNil)
+
+ c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}})
+}
+
+func (s *plugSlotRulesSuite) TestCompileSlotRuleErrors(c *C) {
+ tests := []struct {
+ stanza string
+ err string
+ }{
+ {`iface: foo`, `slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ - allow`, `slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ allow-installation: foo`, `allow-installation in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ deny-installation: foo`, `deny-installation in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ allow-connection: foo`, `allow-connection in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`},
+ {`iface:
+ allow-connection:
+ - foo`, `alternative 1 of allow-connection in slot rule for interface "iface" must be a map`},
+ {`iface:
+ allow-connection:
+ - true`, `alternative 1 of allow-connection in slot rule for interface "iface" must be a map`},
+ {`iface:
+ allow-installation:
+ slot-attributes:
+ a1: [`, `cannot compile slot-attributes in allow-installation in slot rule for interface "iface": cannot compile "a1" constraint .*`},
+ {`iface:
+ allow-connection:
+ plug-attributes:
+ a2: [`, `cannot compile plug-attributes in allow-connection in slot rule for interface "iface": cannot compile "a2" constraint .*`},
+ {`iface:
+ allow-connection:
+ plug-snap-id:
+ -
+ foo: 1`, `plug-snap-id in allow-connection in slot rule for interface "iface" must be a list of strings`},
+ {`iface:
+ allow-connection:
+ plug-snap-id:
+ - foo`, `plug-snap-id in allow-connection in slot rule for interface "iface" contains an invalid element: "foo"`},
+ {`iface:
+ allow-connection:
+ plug-snap-type:
+ - foo`, `plug-snap-type in allow-connection in slot rule for interface "iface" contains an invalid element: "foo"`},
+ {`iface:
+ allow-connection:
+ plug-snap-type:
+ - xapp`, `plug-snap-type in allow-connection in slot rule for interface "iface" contains an invalid element: "xapp"`},
+ {`iface:
+ allow-connection:
+ on-classic:
+ x: 1`, `on-classic in allow-connection in slot rule for interface \"iface\" must be 'true', 'false' or a list of operating system IDs`},
+ {`iface:
+ allow-connection:
+ on-classic:
+ - zoom!`, `on-classic in allow-connection in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`},
+ {`iface:
+ allow-connection:
+ plug-snap-ids:
+ - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`},
+ {`iface:
+ deny-connection:
+ plug-snap-ids:
+ - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`},
+ {`iface:
+ allow-auto-connection:
+ plug-snap-ids:
+ - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`},
+ {`iface:
+ deny-auto-connection:
+ plug-snap-ids:
+ - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`},
+ {`iface:
+ allow-connect: true`, `slot rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`},
+ }
+
+ for _, t := range tests {
+ m, err := asserts.ParseHeaders([]byte(t.stanza))
+ c.Assert(err, IsNil, Commentf(t.stanza))
+ _, err = asserts.CompileSlotRule("iface", m["iface"])
+ c.Check(err, ErrorMatches, t.err, Commentf(t.stanza))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "sync"
+)
+
+type memoryBackstore struct {
+ top memBSBranch
+ mu sync.RWMutex
+}
+
+type memBSNode interface {
+ put(assertType *AssertionType, key []string, assert Assertion) error
+ get(key []string, maxFormat int) (Assertion, error)
+ search(hint []string, found func(Assertion), maxFormat int)
+}
+
+type memBSBranch map[string]memBSNode
+
+type memBSLeaf map[string]map[int]Assertion
+
+func (br memBSBranch) put(assertType *AssertionType, key []string, assert Assertion) error {
+ key0 := key[0]
+ down := br[key0]
+ if down == nil {
+ if len(key) > 2 {
+ down = make(memBSBranch)
+ } else {
+ down = make(memBSLeaf)
+ }
+ br[key0] = down
+ }
+ return down.put(assertType, key[1:], assert)
+}
+
+func (leaf memBSLeaf) cur(key0 string, maxFormat int) (a Assertion) {
+ for formatnum, a1 := range leaf[key0] {
+ if formatnum <= maxFormat {
+ if a == nil || a1.Revision() > a.Revision() {
+ a = a1
+ }
+ }
+ }
+ return a
+}
+
+func (leaf memBSLeaf) put(assertType *AssertionType, key []string, assert Assertion) error {
+ key0 := key[0]
+ cur := leaf.cur(key0, assertType.MaxSupportedFormat())
+ if cur != nil {
+ rev := assert.Revision()
+ curRev := cur.Revision()
+ if curRev >= rev {
+ return &RevisionError{Current: curRev, Used: rev}
+ }
+ }
+ if _, ok := leaf[key0]; !ok {
+ leaf[key0] = make(map[int]Assertion)
+ }
+ leaf[key0][assert.Format()] = assert
+ return nil
+}
+
+func (br memBSBranch) get(key []string, maxFormat int) (Assertion, error) {
+ key0 := key[0]
+ down := br[key0]
+ if down == nil {
+ return nil, ErrNotFound
+ }
+ return down.get(key[1:], maxFormat)
+}
+
+func (leaf memBSLeaf) get(key []string, maxFormat int) (Assertion, error) {
+ key0 := key[0]
+ cur := leaf.cur(key0, maxFormat)
+ if cur == nil {
+ return nil, ErrNotFound
+ }
+ return cur, nil
+}
+
+func (br memBSBranch) search(hint []string, found func(Assertion), maxFormat int) {
+ hint0 := hint[0]
+ if hint0 == "" {
+ for _, down := range br {
+ down.search(hint[1:], found, maxFormat)
+ }
+ return
+ }
+ down := br[hint0]
+ if down != nil {
+ down.search(hint[1:], found, maxFormat)
+ }
+ return
+}
+
+func (leaf memBSLeaf) search(hint []string, found func(Assertion), maxFormat int) {
+ hint0 := hint[0]
+ if hint0 == "" {
+ for key := range leaf {
+ cand := leaf.cur(key, maxFormat)
+ if cand != nil {
+ found(cand)
+ }
+ }
+ return
+ }
+
+ cur := leaf.cur(hint0, maxFormat)
+ if cur != nil {
+ found(cur)
+ }
+}
+
+// NewMemoryBackstore creates a memory backed assertions backstore.
+func NewMemoryBackstore() Backstore {
+ return &memoryBackstore{
+ top: make(memBSBranch),
+ }
+}
+
+func (mbs *memoryBackstore) Put(assertType *AssertionType, assert Assertion) error {
+ mbs.mu.Lock()
+ defer mbs.mu.Unlock()
+
+ internalKey := make([]string, 1+len(assertType.PrimaryKey))
+ internalKey[0] = assertType.Name
+ for i, name := range assertType.PrimaryKey {
+ internalKey[1+i] = assert.HeaderString(name)
+ }
+
+ err := mbs.top.put(assertType, internalKey, assert)
+ return err
+}
+
+func (mbs *memoryBackstore) Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) {
+ mbs.mu.RLock()
+ defer mbs.mu.RUnlock()
+
+ internalKey := make([]string, 1+len(assertType.PrimaryKey))
+ internalKey[0] = assertType.Name
+ copy(internalKey[1:], key)
+
+ return mbs.top.get(internalKey, maxFormat)
+}
+
+func (mbs *memoryBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error {
+ mbs.mu.RLock()
+ defer mbs.mu.RUnlock()
+
+ hint := make([]string, 1+len(assertType.PrimaryKey))
+ hint[0] = assertType.Name
+ for i, name := range assertType.PrimaryKey {
+ hint[1+i] = headers[name]
+ }
+
+ candCb := func(a Assertion) {
+ if searchMatch(a, headers) {
+ foundCb(a)
+ }
+ }
+
+ mbs.top.search(hint, candCb, maxFormat)
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type memBackstoreSuite struct {
+ bs asserts.Backstore
+ a asserts.Assertion
+}
+
+var _ = Suite(&memBackstoreSuite{})
+
+func (mbss *memBackstoreSuite) SetUpTest(c *C) {
+ mbss.bs = asserts.NewMemoryBackstore()
+
+ encoded := "type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ mbss.a = a
+}
+
+func (mbss *memBackstoreSuite) TestPutAndGet(c *C) {
+ err := mbss.bs.Put(asserts.TestOnlyType, mbss.a)
+ c.Assert(err, IsNil)
+
+ a, err := mbss.bs.Get(asserts.TestOnlyType, []string{"foo"}, 0)
+ c.Assert(err, IsNil)
+
+ c.Check(a, Equals, mbss.a)
+}
+
+func (mbss *memBackstoreSuite) TestGetNotFound(c *C) {
+ a, err := mbss.bs.Get(asserts.TestOnlyType, []string{"foo"}, 0)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+ c.Check(a, IsNil)
+
+ err = mbss.bs.Put(asserts.TestOnlyType, mbss.a)
+ c.Assert(err, IsNil)
+
+ a, err = mbss.bs.Get(asserts.TestOnlyType, []string{"bar"}, 0)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+ c.Check(a, IsNil)
+}
+
+func (mbss *memBackstoreSuite) TestPutNotNewer(c *C) {
+ err := mbss.bs.Put(asserts.TestOnlyType, mbss.a)
+ c.Assert(err, IsNil)
+
+ err = mbss.bs.Put(asserts.TestOnlyType, mbss.a)
+ c.Check(err, ErrorMatches, "revision 0 is already the current revision")
+}
+
+func (mbss *memBackstoreSuite) TestSearch(c *C) {
+ encoded := "type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: one\n" +
+ "other: other1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a1, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ encoded = "type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: two\n" +
+ "other: other2\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a2, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ err = mbss.bs.Put(asserts.TestOnlyType, a1)
+ c.Assert(err, IsNil)
+ err = mbss.bs.Put(asserts.TestOnlyType, a2)
+ c.Assert(err, IsNil)
+
+ found := map[string]asserts.Assertion{}
+ cb := func(a asserts.Assertion) {
+ found[a.HeaderString("primary-key")] = a
+ }
+ err = mbss.bs.Search(asserts.TestOnlyType, nil, cb, 0)
+ c.Assert(err, IsNil)
+ c.Check(found, HasLen, 2)
+
+ found = map[string]asserts.Assertion{}
+ err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{
+ "primary-key": "one",
+ }, cb, 0)
+ c.Assert(err, IsNil)
+ c.Check(found, DeepEquals, map[string]asserts.Assertion{
+ "one": a1,
+ })
+
+ found = map[string]asserts.Assertion{}
+ err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{
+ "other": "other2",
+ }, cb, 0)
+ c.Assert(err, IsNil)
+ c.Check(found, DeepEquals, map[string]asserts.Assertion{
+ "two": a2,
+ })
+
+ found = map[string]asserts.Assertion{}
+ err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{
+ "primary-key": "two",
+ "other": "other1",
+ }, cb, 0)
+ c.Assert(err, IsNil)
+ c.Check(found, HasLen, 0)
+}
+
+func (mbss *memBackstoreSuite) TestSearch2Levels(c *C) {
+ encoded := "type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: a\n" +
+ "pk2: x\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ aAX, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ encoded = "type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: b\n" +
+ "pk2: x\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ aBX, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ err = mbss.bs.Put(asserts.TestOnly2Type, aAX)
+ c.Assert(err, IsNil)
+ err = mbss.bs.Put(asserts.TestOnly2Type, aBX)
+ c.Assert(err, IsNil)
+
+ found := map[string]asserts.Assertion{}
+ cb := func(a asserts.Assertion) {
+ found[a.HeaderString("pk1")+":"+a.HeaderString("pk2")] = a
+ }
+ err = mbss.bs.Search(asserts.TestOnly2Type, map[string]string{
+ "pk2": "x",
+ }, cb, 0)
+ c.Assert(err, IsNil)
+ c.Check(found, HasLen, 2)
+}
+
+func (mbss *memBackstoreSuite) TestPutOldRevision(c *C) {
+ bs := asserts.NewMemoryBackstore()
+
+ // Create two revisions of assertion.
+ a0, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ a1, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ // Put newer revision, follwed by old revision.
+ err = bs.Put(asserts.TestOnlyType, a1)
+ c.Assert(err, IsNil)
+ err = bs.Put(asserts.TestOnlyType, a0)
+
+ c.Check(err, ErrorMatches, `revision 0 is older than current revision 1`)
+ c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0})
+}
+
+func (mbss *memBackstoreSuite) TestGetFormat(c *C) {
+ bs := asserts.NewMemoryBackstore()
+
+ af0, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ af1, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: foo\n" +
+ "format: 1\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ af2, err := asserts.Decode([]byte("type: test-only\n" +
+ "authority-id: auth-id1\n" +
+ "primary-key: zoo\n" +
+ "format: 2\n" +
+ "revision: 22\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ err = bs.Put(asserts.TestOnlyType, af0)
+ c.Assert(err, IsNil)
+ err = bs.Put(asserts.TestOnlyType, af1)
+ c.Assert(err, IsNil)
+
+ a, err := bs.Get(asserts.TestOnlyType, []string{"foo"}, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 1)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"foo"}, 0)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 0)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+
+ err = bs.Put(asserts.TestOnlyType, af2)
+ c.Assert(err, IsNil)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+
+ a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 2)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 22)
+}
+
+func (mbss *memBackstoreSuite) TestSearchFormat(c *C) {
+ bs := asserts.NewMemoryBackstore()
+
+ af0, err := asserts.Decode([]byte("type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: foo\n" +
+ "pk2: bar\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+ af1, err := asserts.Decode([]byte("type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: foo\n" +
+ "pk2: bar\n" +
+ "format: 1\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ af2, err := asserts.Decode([]byte("type: test-only-2\n" +
+ "authority-id: auth-id1\n" +
+ "pk1: foo\n" +
+ "pk2: baz\n" +
+ "format: 2\n" +
+ "revision: 1\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="))
+ c.Assert(err, IsNil)
+
+ err = bs.Put(asserts.TestOnly2Type, af0)
+ c.Assert(err, IsNil)
+
+ queries := []map[string]string{
+ {"pk1": "foo", "pk2": "bar"},
+ {"pk1": "foo"},
+ {"pk2": "bar"},
+ }
+
+ for _, q := range queries {
+ var a asserts.Assertion
+ foundCb := func(a1 asserts.Assertion) {
+ a = a1
+ }
+ err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+ }
+
+ err = bs.Put(asserts.TestOnly2Type, af1)
+ c.Assert(err, IsNil)
+
+ for _, q := range queries {
+ var a asserts.Assertion
+ foundCb := func(a1 asserts.Assertion) {
+ a = a1
+ }
+ err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 1)
+
+ err = bs.Search(asserts.TestOnly2Type, q, foundCb, 0)
+ c.Assert(err, IsNil)
+ c.Check(a.Revision(), Equals, 0)
+ }
+
+ err = bs.Put(asserts.TestOnly2Type, af2)
+ c.Assert(err, IsNil)
+
+ var as []asserts.Assertion
+ foundCb := func(a1 asserts.Assertion) {
+ as = append(as, a1)
+ }
+ err = bs.Search(asserts.TestOnly2Type, map[string]string{
+ "pk1": "foo",
+ }, foundCb, 1) // will not find af2
+ c.Assert(err, IsNil)
+ c.Check(as, HasLen, 1)
+ c.Check(as[0].Revision(), Equals, 1)
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "sync"
+)
+
+type memoryKeypairManager struct {
+ pairs map[string]PrivateKey
+ mu sync.RWMutex
+}
+
+// NewMemoryKeypairManager creates a new key pair manager with a memory backstore.
+func NewMemoryKeypairManager() KeypairManager {
+ return &memoryKeypairManager{
+ pairs: make(map[string]PrivateKey),
+ }
+}
+
+func (mkm *memoryKeypairManager) Put(privKey PrivateKey) error {
+ mkm.mu.Lock()
+ defer mkm.mu.Unlock()
+
+ keyID := privKey.PublicKey().ID()
+ if mkm.pairs[keyID] != nil {
+ return errKeypairAlreadyExists
+ }
+ mkm.pairs[keyID] = privKey
+ return nil
+}
+
+func (mkm *memoryKeypairManager) Get(keyID string) (PrivateKey, error) {
+ mkm.mu.RLock()
+ defer mkm.mu.RUnlock()
+
+ privKey := mkm.pairs[keyID]
+ if privKey == nil {
+ return nil, errKeypairNotFound
+ }
+ return privKey, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type memKeypairMgtSuite struct {
+ keypairMgr asserts.KeypairManager
+}
+
+var _ = Suite(&memKeypairMgtSuite{})
+
+func (mkms *memKeypairMgtSuite) SetUpTest(c *C) {
+ mkms.keypairMgr = asserts.NewMemoryKeypairManager()
+}
+
+func (mkms *memKeypairMgtSuite) TestPutAndGet(c *C) {
+ pk1 := testPrivKey1
+ keyID := pk1.PublicKey().ID()
+ err := mkms.keypairMgr.Put(pk1)
+ c.Assert(err, IsNil)
+
+ got, err := mkms.keypairMgr.Get(keyID)
+ c.Assert(err, IsNil)
+ c.Assert(got, NotNil)
+ c.Check(got.PublicKey().ID(), Equals, pk1.PublicKey().ID())
+}
+
+func (mkms *memKeypairMgtSuite) TestPutAlreadyExists(c *C) {
+ pk1 := testPrivKey1
+ err := mkms.keypairMgr.Put(pk1)
+ c.Assert(err, IsNil)
+
+ err = mkms.keypairMgr.Put(pk1)
+ c.Check(err, ErrorMatches, "key pair with given key id already exists")
+}
+
+func (mkms *memKeypairMgtSuite) TestGetNotFound(c *C) {
+ pk1 := testPrivKey1
+ keyID := pk1.PublicKey().ID()
+
+ got, err := mkms.keypairMgr.Get(keyID)
+ c.Check(got, IsNil)
+ c.Check(err, ErrorMatches, "cannot find key pair")
+
+ err = mkms.keypairMgr.Put(pk1)
+ c.Assert(err, IsNil)
+
+ got, err = mkms.keypairMgr.Get(keyID + "x")
+ c.Check(got, IsNil)
+ c.Check(err, ErrorMatches, "cannot find key pair")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "encoding/base64"
+
+ "golang.org/x/crypto/openpgp/packet"
+ "golang.org/x/crypto/sha3"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+// private keys to use in tests
+var (
+ // use a shorter key length here for test keys because otherwise
+ // they take too long to generate;
+ // the ones that care use pregenerated keys of the right length
+ // or use GenerateKey directly
+ testPrivKey0, _ = assertstest.GenerateKey(752)
+ testPrivKey1, testPrivKey1RSA = assertstest.GenerateKey(752)
+ testPrivKey2, _ = assertstest.GenerateKey(752)
+
+ testPrivKey1SHA3_384 string
+)
+
+func init() {
+ pkt := packet.NewRSAPrivateKey(asserts.V1FixedTimestamp, testPrivKey1RSA)
+ h := sha3.New384()
+ h.Write([]byte{0x1})
+ err := pkt.PublicKey.Serialize(h)
+ if err != nil {
+ panic(err)
+ }
+ testPrivKey1SHA3_384 = base64.RawURLEncoding.EncodeToString(h.Sum(nil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package signtool offers tooling to sign assertions.
+package signtool
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+// Options specifies the complete input for signing an assertion.
+type Options struct {
+ // KeyID specifies the key id of the key to use
+ KeyID string
+
+ // Statement is used as input to construct the assertion
+ // it's a mapping encoded as JSON
+ // of the header fields of the assertion
+ // plus an optional pseudo-header "body" to specify
+ // the body of the assertion
+ Statement []byte
+}
+
+// Sign produces the text of a signed assertion as specified by opts.
+func Sign(opts *Options, keypairMgr asserts.KeypairManager) ([]byte, error) {
+ var headers map[string]interface{}
+ err := json.Unmarshal(opts.Statement, &headers)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse the assertion input as JSON: %v", err)
+ }
+ typCand, ok := headers["type"]
+ if !ok {
+ return nil, fmt.Errorf("missing assertion type header")
+ }
+ typStr, ok := typCand.(string)
+ if !ok {
+ return nil, fmt.Errorf("assertion type must be a string, not: %v", typCand)
+ }
+ typ := asserts.Type(typStr)
+ if typ == nil {
+ return nil, fmt.Errorf("invalid assertion type: %v", headers["type"])
+ }
+
+ var body []byte
+ if bodyCand, ok := headers["body"]; ok {
+ bodyStr, ok := bodyCand.(string)
+ if !ok {
+ return nil, fmt.Errorf("body if specified must be a string")
+ }
+ body = []byte(bodyStr)
+ delete(headers, "body")
+ }
+
+ adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: keypairMgr,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO: teach Sign to cross check keyID and authority-id
+ // against an account-key
+ a, err := adb.Sign(typ, headers, body, opts.KeyID)
+ if err != nil {
+ return nil, err
+ }
+
+ return asserts.Encode(a), nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package signtool_test
+
+import (
+ "encoding/json"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/signtool"
+)
+
+func TestSigntool(t *testing.T) { TestingT(t) }
+
+type signSuite struct {
+ keypairMgr asserts.KeypairManager
+ testKeyID string
+}
+
+var _ = Suite(&signSuite{})
+
+func (s *signSuite) SetUpSuite(c *C) {
+ testKey, _ := assertstest.GenerateKey(752)
+
+ s.keypairMgr = asserts.NewMemoryKeypairManager()
+ s.keypairMgr.Put(testKey)
+ s.testKeyID = testKey.PublicKey().ID()
+}
+
+func expectedModelHeaders(a asserts.Assertion) map[string]interface{} {
+ m := map[string]interface{}{
+ "type": "model",
+ "authority-id": "user-id1",
+ "series": "16",
+ "brand-id": "user-id1",
+ "model": "baz-3000",
+ "architecture": "amd64",
+ "gadget": "brand-gadget",
+ "kernel": "baz-linux",
+ "store": "brand-store",
+ "required-snaps": []interface{}{"foo", "bar"},
+ "timestamp": "2015-11-25T20:00:00Z",
+ }
+ if a != nil {
+ m["sign-key-sha3-384"] = a.SignKeyID()
+ }
+ return m
+}
+
+func exampleJSON(overrides map[string]interface{}) []byte {
+ m := expectedModelHeaders(nil)
+ for k, v := range overrides {
+ if v == nil {
+ delete(m, k)
+ } else {
+ m[k] = v
+ }
+ }
+ b, err := json.Marshal(m)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+func (s *signSuite) TestSignJSON(c *C) {
+ opts := signtool.Options{
+ KeyID: s.testKeyID,
+
+ Statement: exampleJSON(nil),
+ }
+
+ assertText, err := signtool.Sign(&opts, s.keypairMgr)
+ c.Assert(err, IsNil)
+
+ a, err := asserts.Decode(assertText)
+ c.Assert(err, IsNil)
+
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ c.Check(a.Revision(), Equals, 0)
+ expectedHeaders := expectedModelHeaders(a)
+ c.Check(a.Headers(), DeepEquals, expectedHeaders)
+
+ for n, v := range a.Headers() {
+ c.Check(v, DeepEquals, expectedHeaders[n], Commentf(n))
+ }
+
+ c.Check(a.Body(), IsNil)
+}
+
+func (s *signSuite) TestSignJSONWithBodyAndRevision(c *C) {
+ statement := exampleJSON(map[string]interface{}{
+ "body": "BODY",
+ "revision": "11",
+ })
+ opts := signtool.Options{
+ KeyID: s.testKeyID,
+
+ Statement: statement,
+ }
+
+ assertText, err := signtool.Sign(&opts, s.keypairMgr)
+ c.Assert(err, IsNil)
+
+ a, err := asserts.Decode(assertText)
+ c.Assert(err, IsNil)
+
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ c.Check(a.Revision(), Equals, 11)
+
+ expectedHeaders := expectedModelHeaders(a)
+ expectedHeaders["revision"] = "11"
+ expectedHeaders["body-length"] = "4"
+
+ c.Check(a.Headers(), DeepEquals, expectedHeaders)
+
+ c.Check(a.Body(), DeepEquals, []byte("BODY"))
+}
+
+func (s *signSuite) TestSignErrors(c *C) {
+ opts := signtool.Options{
+ KeyID: s.testKeyID,
+ }
+
+ emptyList := []interface{}{}
+
+ tests := []struct {
+ expError string
+ brokenStatement []byte
+ }{
+ {`cannot parse the assertion input as JSON:.*`,
+ []byte("\x00"),
+ },
+ {`invalid assertion type: what`,
+ exampleJSON(map[string]interface{}{"type": "what"}),
+ },
+ {`assertion type must be a string, not: \[\]`,
+ exampleJSON(map[string]interface{}{"type": emptyList}),
+ },
+ {`missing assertion type header`,
+ exampleJSON(map[string]interface{}{"type": nil}),
+ },
+ {"revision should be positive: -10",
+ exampleJSON(map[string]interface{}{"revision": "-10"})},
+ {`"authority-id" header is mandatory`,
+ exampleJSON(map[string]interface{}{"authority-id": nil})},
+ {`body if specified must be a string`,
+ exampleJSON(map[string]interface{}{"body": emptyList})},
+ }
+
+ for _, t := range tests {
+ fresh := opts
+
+ fresh.Statement = t.brokenStatement
+
+ _, err := signtool.Sign(&fresh, s.keypairMgr)
+ c.Check(err, ErrorMatches, t.expError)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "bytes"
+ "crypto"
+ "fmt"
+ "regexp"
+ "time"
+
+ _ "golang.org/x/crypto/sha3" // expected for digests
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/release"
+)
+
+// SnapDeclaration holds a snap-declaration assertion, declaring a
+// snap binding its identifying snap-id to a name, asserting its
+// publisher and its other properties.
+type SnapDeclaration struct {
+ assertionBase
+ refreshControl []string
+ plugRules map[string]*PlugRule
+ slotRules map[string]*SlotRule
+ autoAliases []string
+ timestamp time.Time
+}
+
+// Series returns the series for which the snap is being declared.
+func (snapdcl *SnapDeclaration) Series() string {
+ return snapdcl.HeaderString("series")
+}
+
+// SnapID returns the snap id of the declared snap.
+func (snapdcl *SnapDeclaration) SnapID() string {
+ return snapdcl.HeaderString("snap-id")
+}
+
+// SnapName returns the declared snap name.
+func (snapdcl *SnapDeclaration) SnapName() string {
+ return snapdcl.HeaderString("snap-name")
+}
+
+// PublisherID returns the identifier of the publisher of the declared snap.
+func (snapdcl *SnapDeclaration) PublisherID() string {
+ return snapdcl.HeaderString("publisher-id")
+}
+
+// Timestamp returns the time when the snap-declaration was issued.
+func (snapdcl *SnapDeclaration) Timestamp() time.Time {
+ return snapdcl.timestamp
+}
+
+// RefreshControl returns the ids of snaps whose updates are controlled by this declaration.
+func (snapdcl *SnapDeclaration) RefreshControl() []string {
+ return snapdcl.refreshControl
+}
+
+// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil.
+func (snapdcl *SnapDeclaration) PlugRule(interfaceName string) *PlugRule {
+ return snapdcl.plugRules[interfaceName]
+}
+
+// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil.
+func (snapdcl *SnapDeclaration) SlotRule(interfaceName string) *SlotRule {
+ return snapdcl.slotRules[interfaceName]
+}
+
+// AutoAliases returns the optional auto-aliases granted to this snap.
+func (snapdcl *SnapDeclaration) AutoAliases() []string {
+ return snapdcl.autoAliases
+}
+
+// Implement further consistency checks.
+func (snapdcl *SnapDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error {
+ if !db.IsTrustedAccount(snapdcl.AuthorityID()) {
+ return fmt.Errorf("snap-declaration assertion for %q (id %q) is not signed by a directly trusted authority: %s", snapdcl.SnapName(), snapdcl.SnapID(), snapdcl.AuthorityID())
+ }
+ _, err := db.Find(AccountType, map[string]string{
+ "account-id": snapdcl.PublisherID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("snap-declaration assertion for %q (id %q) does not have a matching account assertion for the publisher %q", snapdcl.SnapName(), snapdcl.SnapID(), snapdcl.PublisherID())
+ }
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*SnapDeclaration)(nil)
+
+// Prerequisites returns references to this snap-declaration's prerequisite assertions.
+func (snapdcl *SnapDeclaration) Prerequisites() []*Ref {
+ return []*Ref{
+ {Type: AccountType, PrimaryKey: []string{snapdcl.PublisherID()}},
+ }
+}
+
+var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$")
+
+func assembleSnapDeclaration(assert assertionBase) (Assertion, error) {
+ _, err := checkExistsString(assert.headers, "snap-name")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "publisher-id")
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ var refControl []string
+ var plugRules map[string]*PlugRule
+ var slotRules map[string]*SlotRule
+
+ refControl, err = checkStringList(assert.headers, "refresh-control")
+ if err != nil {
+ return nil, err
+ }
+
+ plugs, err := checkMap(assert.headers, "plugs")
+ if err != nil {
+ return nil, err
+ }
+ if plugs != nil {
+ plugRules = make(map[string]*PlugRule, len(plugs))
+ for iface, rule := range plugs {
+ plugRule, err := compilePlugRule(iface, rule)
+ if err != nil {
+ return nil, err
+ }
+ plugRules[iface] = plugRule
+ }
+ }
+
+ slots, err := checkMap(assert.headers, "slots")
+ if err != nil {
+ return nil, err
+ }
+ if slots != nil {
+ slotRules = make(map[string]*SlotRule, len(slots))
+ for iface, rule := range slots {
+ slotRule, err := compileSlotRule(iface, rule)
+ if err != nil {
+ return nil, err
+ }
+ slotRules[iface] = slotRule
+ }
+ }
+
+ autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias)
+ if err != nil {
+ return nil, err
+ }
+
+ return &SnapDeclaration{
+ assertionBase: assert,
+ refreshControl: refControl,
+ plugRules: plugRules,
+ slotRules: slotRules,
+ autoAliases: autoAliases,
+ timestamp: timestamp,
+ }, nil
+}
+
+// SnapFileSHA3_384 computes the SHA3-384 digest of the given snap file.
+// It also returns its size.
+func SnapFileSHA3_384(snapPath string) (digest string, size uint64, err error) {
+ sha3_384Dgst, size, err := osutil.FileDigest(snapPath, crypto.SHA3_384)
+ if err != nil {
+ return "", 0, fmt.Errorf("cannot compute snap %q digest: %v", snapPath, err)
+ }
+
+ sha3_384, err := EncodeDigest(crypto.SHA3_384, sha3_384Dgst)
+ if err != nil {
+ return "", 0, fmt.Errorf("cannot encode snap %q digest: %v", snapPath, err)
+ }
+ return sha3_384, size, nil
+}
+
+// SnapBuild holds a snap-build assertion, asserting the properties of a snap
+// at the time it was built by the developer.
+type SnapBuild struct {
+ assertionBase
+ size uint64
+ timestamp time.Time
+}
+
+// SnapSHA3_384 returns the SHA3-384 digest of the snap.
+func (snapbld *SnapBuild) SnapSHA3_384() string {
+ return snapbld.HeaderString("snap-sha3-384")
+}
+
+// SnapID returns the snap id of the snap.
+func (snapbld *SnapBuild) SnapID() string {
+ return snapbld.HeaderString("snap-id")
+}
+
+// SnapSize returns the size of the snap.
+func (snapbld *SnapBuild) SnapSize() uint64 {
+ return snapbld.size
+}
+
+// Grade returns the grade of the snap: devel|stable
+func (snapbld *SnapBuild) Grade() string {
+ return snapbld.HeaderString("grade")
+}
+
+// Timestamp returns the time when the snap-build assertion was created.
+func (snapbld *SnapBuild) Timestamp() time.Time {
+ return snapbld.timestamp
+}
+
+func assembleSnapBuild(assert assertionBase) (Assertion, error) {
+ _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "snap-id")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "grade")
+ if err != nil {
+ return nil, err
+ }
+
+ size, err := checkUint(assert.headers, "snap-size", 64)
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+ // ignore extra headers and non-empty body for future compatibility
+ return &SnapBuild{
+ assertionBase: assert,
+ size: size,
+ timestamp: timestamp,
+ }, nil
+}
+
+// SnapRevision holds a snap-revision assertion, which is a statement by the
+// store acknowledging the receipt of a build of a snap and labeling it with a
+// snap revision.
+type SnapRevision struct {
+ assertionBase
+ snapSize uint64
+ snapRevision int
+ timestamp time.Time
+}
+
+// SnapSHA3_384 returns the SHA3-384 digest of the snap.
+func (snaprev *SnapRevision) SnapSHA3_384() string {
+ return snaprev.HeaderString("snap-sha3-384")
+}
+
+// SnapID returns the snap id of the snap.
+func (snaprev *SnapRevision) SnapID() string {
+ return snaprev.HeaderString("snap-id")
+}
+
+// SnapSize returns the size in bytes of the snap submitted to the store.
+func (snaprev *SnapRevision) SnapSize() uint64 {
+ return snaprev.snapSize
+}
+
+// SnapRevision returns the revision assigned to this build of the snap.
+func (snaprev *SnapRevision) SnapRevision() int {
+ return snaprev.snapRevision
+}
+
+// DeveloperID returns the id of the developer that submitted this build of the
+// snap.
+func (snaprev *SnapRevision) DeveloperID() string {
+ return snaprev.HeaderString("developer-id")
+}
+
+// Timestamp returns the time when the snap-revision was issued.
+func (snaprev *SnapRevision) Timestamp() time.Time {
+ return snaprev.timestamp
+}
+
+// Implement further consistency checks.
+func (snaprev *SnapRevision) checkConsistency(db RODatabase, acck *AccountKey) error {
+ // TODO: expand this to consider other stores signing on their own
+ if !db.IsTrustedAccount(snaprev.AuthorityID()) {
+ return fmt.Errorf("snap-revision assertion for snap id %q is not signed by a store: %s", snaprev.SnapID(), snaprev.AuthorityID())
+ }
+ _, err := db.Find(AccountType, map[string]string{
+ "account-id": snaprev.DeveloperID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("snap-revision assertion for snap id %q does not have a matching account assertion for the developer %q", snaprev.SnapID(), snaprev.DeveloperID())
+ }
+ if err != nil {
+ return err
+ }
+ _, err = db.Find(SnapDeclarationType, map[string]string{
+ // XXX: mediate getting current series through some context object? this gets the job done for now
+ "series": release.Series,
+ "snap-id": snaprev.SnapID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("snap-revision assertion for snap id %q does not have a matching snap-declaration assertion", snaprev.SnapID())
+ }
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*SnapRevision)(nil)
+
+// Prerequisites returns references to this snap-revision's prerequisite assertions.
+func (snaprev *SnapRevision) Prerequisites() []*Ref {
+ return []*Ref{
+ // XXX: mediate getting current series through some context object? this gets the job done for now
+ {Type: SnapDeclarationType, PrimaryKey: []string{release.Series, snaprev.SnapID()}},
+ {Type: AccountType, PrimaryKey: []string{snaprev.DeveloperID()}},
+ }
+}
+
+func assembleSnapRevision(assert assertionBase) (Assertion, error) {
+ _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "snap-id")
+ if err != nil {
+ return nil, err
+ }
+
+ snapSize, err := checkUint(assert.headers, "snap-size", 64)
+ if err != nil {
+ return nil, err
+ }
+
+ snapRevision, err := checkInt(assert.headers, "snap-revision")
+ if err != nil {
+ return nil, err
+ }
+ if snapRevision < 1 {
+ return nil, fmt.Errorf(`"snap-revision" header must be >=1: %d`, snapRevision)
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "developer-id")
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ return &SnapRevision{
+ assertionBase: assert,
+ snapSize: snapSize,
+ snapRevision: snapRevision,
+ timestamp: timestamp,
+ }, nil
+}
+
+// Validation holds a validation assertion, describing that a combination of
+// (snap-id, approved-snap-id, approved-revision) has been validated for
+// the series, meaning updating to that revision of approved-snap-id
+// has been approved by the owner of the gating snap with snap-id.
+type Validation struct {
+ assertionBase
+ revoked bool
+ timestamp time.Time
+ approvedSnapRevision int
+}
+
+// Series returns the series for which the validation holds.
+func (validation *Validation) Series() string {
+ return validation.HeaderString("series")
+}
+
+// SnapID returns the ID of the gating snap.
+func (validation *Validation) SnapID() string {
+ return validation.HeaderString("snap-id")
+}
+
+// ApprovedSnapID returns the ID of the gated snap.
+func (validation *Validation) ApprovedSnapID() string {
+ return validation.HeaderString("approved-snap-id")
+}
+
+// ApprovedSnapRevision returns the approved revision of the gated snap.
+func (validation *Validation) ApprovedSnapRevision() int {
+ return validation.approvedSnapRevision
+}
+
+// Revoked returns true if the validation has been revoked.
+func (validation *Validation) Revoked() bool {
+ return validation.revoked
+}
+
+// Timestamp returns the time when the validation was issued.
+func (validation *Validation) Timestamp() time.Time {
+ return validation.timestamp
+}
+
+// Implement further consistency checks.
+func (validation *Validation) checkConsistency(db RODatabase, acck *AccountKey) error {
+ _, err := db.Find(SnapDeclarationType, map[string]string{
+ "series": validation.Series(),
+ "snap-id": validation.ApprovedSnapID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion for approved-snap-id %q", validation.SnapID(), validation.ApprovedSnapID())
+ }
+ if err != nil {
+ return err
+ }
+ a, err := db.Find(SnapDeclarationType, map[string]string{
+ "series": validation.Series(),
+ "snap-id": validation.SnapID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion", validation.SnapID())
+ }
+ if err != nil {
+ return err
+ }
+
+ gatingDecl := a.(*SnapDeclaration)
+ if gatingDecl.PublisherID() != validation.AuthorityID() {
+ return fmt.Errorf("validation assertion by snap %q (id %q) not signed by its publisher", gatingDecl.SnapName(), validation.SnapID())
+ }
+
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*Validation)(nil)
+
+// Prerequisites returns references to this validation's prerequisite assertions.
+func (validation *Validation) Prerequisites() []*Ref {
+ return []*Ref{
+ {Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.SnapID()}},
+ {Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.ApprovedSnapID()}},
+ }
+}
+
+func assembleValidation(assert assertionBase) (Assertion, error) {
+ approvedSnapRevision, err := checkInt(assert.headers, "approved-snap-revision")
+ if err != nil {
+ return nil, err
+ }
+ if approvedSnapRevision < 1 {
+ return nil, fmt.Errorf(`"approved-snap-revision" header must be >=1: %d`, approvedSnapRevision)
+ }
+
+ revoked, err := checkOptionalBool(assert.headers, "revoked")
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ return &Validation{
+ assertionBase: assert,
+ revoked: revoked,
+ timestamp: timestamp,
+ approvedSnapRevision: approvedSnapRevision,
+ }, nil
+}
+
+// BaseDeclaration holds a base-declaration assertion, declaring the
+// policies (to start with interface ones) applying to all snaps of
+// a series.
+type BaseDeclaration struct {
+ assertionBase
+ plugRules map[string]*PlugRule
+ slotRules map[string]*SlotRule
+ timestamp time.Time
+}
+
+// Series returns the series whose snaps are governed by the declaration.
+func (basedcl *BaseDeclaration) Series() string {
+ return basedcl.HeaderString("series")
+}
+
+// Timestamp returns the time when the base-declaration was issued.
+func (basedcl *BaseDeclaration) Timestamp() time.Time {
+ return basedcl.timestamp
+}
+
+// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil.
+func (basedcl *BaseDeclaration) PlugRule(interfaceName string) *PlugRule {
+ return basedcl.plugRules[interfaceName]
+}
+
+// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil.
+func (basedcl *BaseDeclaration) SlotRule(interfaceName string) *SlotRule {
+ return basedcl.slotRules[interfaceName]
+}
+
+// Implement further consistency checks.
+func (basedcl *BaseDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error {
+ // XXX: not signed or stored yet in a db, but being ready for that
+ if !db.IsTrustedAccount(basedcl.AuthorityID()) {
+ return fmt.Errorf("base-declaration assertion for series %s is not signed by a directly trusted authority: %s", basedcl.Series(), basedcl.AuthorityID())
+ }
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*BaseDeclaration)(nil)
+
+func assembleBaseDeclaration(assert assertionBase) (Assertion, error) {
+ var plugRules map[string]*PlugRule
+ plugs, err := checkMap(assert.headers, "plugs")
+ if err != nil {
+ return nil, err
+ }
+ if plugs != nil {
+ plugRules = make(map[string]*PlugRule, len(plugs))
+ for iface, rule := range plugs {
+ plugRule, err := compilePlugRule(iface, rule)
+ if err != nil {
+ return nil, err
+ }
+ plugRules[iface] = plugRule
+ }
+ }
+
+ var slotRules map[string]*SlotRule
+ slots, err := checkMap(assert.headers, "slots")
+ if err != nil {
+ return nil, err
+ }
+ if slots != nil {
+ slotRules = make(map[string]*SlotRule, len(slots))
+ for iface, rule := range slots {
+ slotRule, err := compileSlotRule(iface, rule)
+ if err != nil {
+ return nil, err
+ }
+ slotRules[iface] = slotRule
+ }
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ return &BaseDeclaration{
+ assertionBase: assert,
+ plugRules: plugRules,
+ slotRules: slotRules,
+ timestamp: timestamp,
+ }, nil
+}
+
+var builtinBaseDeclaration *BaseDeclaration
+
+// BuiltinBaseDeclaration exposes the initialized builtin base-declaration assertion. This is used by overlord/assertstate, other code should use assertstate.BaseDeclaration.
+func BuiltinBaseDeclaration() *BaseDeclaration {
+ return builtinBaseDeclaration
+}
+
+var (
+ builtinBaseDeclarationCheckOrder = []string{"type", "authority-id", "series"}
+ builtinBaseDeclarationExpectedHeaders = map[string]interface{}{
+ "type": "base-declaration",
+ "authority-id": "canonical",
+ "series": release.Series,
+ }
+)
+
+// InitBuiltinBaseDeclaration initializes the builtin base-declaration based on headers (or resets it if headers is nil).
+func InitBuiltinBaseDeclaration(headers []byte) error {
+ if headers == nil {
+ builtinBaseDeclaration = nil
+ return nil
+ }
+ trimmed := bytes.TrimSpace(headers)
+ h, err := parseHeaders(trimmed)
+ if err != nil {
+ return err
+ }
+ for _, name := range builtinBaseDeclarationCheckOrder {
+ expected := builtinBaseDeclarationExpectedHeaders[name]
+ if h[name] != expected {
+ return fmt.Errorf("the builtin base-declaration %q header is not set to expected value %q", name, expected)
+ }
+ }
+ revision, err := checkRevision(h)
+ if err != nil {
+ return fmt.Errorf("cannot assemble the builtin-base declaration: %v", err)
+ }
+ h["timestamp"] = time.Now().UTC().Format(time.RFC3339)
+ a, err := assembleBaseDeclaration(assertionBase{
+ headers: h,
+ body: nil,
+ revision: revision,
+ content: trimmed,
+ signature: []byte("$builtin"),
+ })
+ if err != nil {
+ return fmt.Errorf("cannot assemble the builtin base-declaration: %v", err)
+ }
+ builtinBaseDeclaration = a.(*BaseDeclaration)
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "encoding/base64"
+ "io/ioutil"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/sha3"
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+var (
+ _ = Suite(&snapDeclSuite{})
+ _ = Suite(&snapFileDigestSuite{})
+ _ = Suite(&snapBuildSuite{})
+ _ = Suite(&snapRevSuite{})
+ _ = Suite(&validationSuite{})
+ _ = Suite(&baseDeclSuite{})
+)
+
+type snapDeclSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+func (sds *snapDeclSuite) SetUpSuite(c *C) {
+ sds.ts = time.Now().Truncate(time.Second).UTC()
+ sds.tsLine = "timestamp: " + sds.ts.Format(time.RFC3339) + "\n"
+}
+
+func (sds *snapDeclSuite) TestDecodeOK(c *C) {
+ encoded := "type: snap-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-name: first\n" +
+ "publisher-id: dev-id1\n" +
+ "refresh-control:\n - foo\n - bar\n" +
+ "auto-aliases:\n - cmd1\n - cmd_2\n - Cmd-3\n - CMD.4\n" +
+ sds.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SnapDeclarationType)
+ snapDecl := a.(*asserts.SnapDeclaration)
+ c.Check(snapDecl.AuthorityID(), Equals, "canonical")
+ c.Check(snapDecl.Timestamp(), Equals, sds.ts)
+ c.Check(snapDecl.Series(), Equals, "16")
+ c.Check(snapDecl.SnapID(), Equals, "snap-id-1")
+ c.Check(snapDecl.SnapName(), Equals, "first")
+ c.Check(snapDecl.PublisherID(), Equals, "dev-id1")
+ c.Check(snapDecl.RefreshControl(), DeepEquals, []string{"foo", "bar"})
+ c.Check(snapDecl.AutoAliases(), DeepEquals, []string{"cmd1", "cmd_2", "Cmd-3", "CMD.4"})
+}
+
+func (sds *snapDeclSuite) TestEmptySnapName(c *C) {
+ encoded := "type: snap-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-name: \n" +
+ "publisher-id: dev-id1\n" +
+ sds.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ snapDecl := a.(*asserts.SnapDeclaration)
+ c.Check(snapDecl.SnapName(), Equals, "")
+}
+
+func (sds *snapDeclSuite) TestMissingRefreshControlAutoAliases(c *C) {
+ encoded := "type: snap-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-name: \n" +
+ "publisher-id: dev-id1\n" +
+ sds.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ snapDecl := a.(*asserts.SnapDeclaration)
+ c.Check(snapDecl.RefreshControl(), HasLen, 0)
+ c.Check(snapDecl.AutoAliases(), HasLen, 0)
+}
+
+const (
+ snapDeclErrPrefix = "assertion snap-declaration: "
+)
+
+func (sds *snapDeclSuite) TestDecodeInvalid(c *C) {
+ encoded := "type: snap-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-name: first\n" +
+ "publisher-id: dev-id1\n" +
+ "refresh-control:\n - foo\n - bar\n" +
+ "auto-aliases:\n - cmd1\n - cmd2\n" +
+ "plugs:\n interface1: true\n" +
+ "slots:\n interface2: true\n" +
+ sds.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"series: 16\n", "", `"series" header is mandatory`},
+ {"series: 16\n", "series: \n", `"series" header should not be empty`},
+ {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`},
+ {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`},
+ {"snap-name: first\n", "", `"snap-name" header is mandatory`},
+ {"publisher-id: dev-id1\n", "", `"publisher-id" header is mandatory`},
+ {"publisher-id: dev-id1\n", "publisher-id: \n", `"publisher-id" header should not be empty`},
+ {"refresh-control:\n - foo\n - bar\n", "refresh-control: foo\n", `"refresh-control" header must be a list of strings`},
+ {"refresh-control:\n - foo\n - bar\n", "refresh-control:\n -\n - nested\n", `"refresh-control" header must be a list of strings`},
+ {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`},
+ {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`},
+ {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`},
+ {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`},
+ {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases: cmd0\n", `"auto-aliases" header must be a list of strings`},
+ {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases:\n -\n - nested\n", `"auto-aliases" header must be a list of strings`},
+ {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases:\n - _cmd-1\n - cmd2\n", `"auto-aliases" header contains an invalid element: "_cmd-1"`},
+ {sds.tsLine, "", `"timestamp" header is mandatory`},
+ {sds.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {sds.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, snapDeclErrPrefix+test.expectedErr)
+ }
+
+}
+
+func (sds *snapDeclSuite) TestDecodePlugsAndSlots(c *C) {
+ encoded := `type: snap-declaration
+authority-id: canonical
+series: 16
+snap-id: snap-id-1
+snap-name: first
+publisher-id: dev-id1
+plugs:
+ interface1:
+ deny-installation: false
+ allow-auto-connection:
+ slot-snap-type:
+ - app
+ slot-publisher-id:
+ - acme
+ slot-attributes:
+ a1: /foo/.*
+ plug-attributes:
+ b1: B1
+ deny-auto-connection:
+ slot-attributes:
+ a1: !A1
+ plug-attributes:
+ b1: !B1
+ interface2:
+ allow-installation: true
+ allow-connection:
+ plug-attributes:
+ a2: A2
+ slot-attributes:
+ b2: B2
+ deny-connection:
+ slot-snap-id:
+ - snapidsnapidsnapidsnapidsnapid01
+ - snapidsnapidsnapidsnapidsnapid02
+ plug-attributes:
+ a2: !A2
+ slot-attributes:
+ b2: !B2
+slots:
+ interface3:
+ deny-installation: false
+ allow-auto-connection:
+ plug-snap-type:
+ - app
+ plug-publisher-id:
+ - acme
+ slot-attributes:
+ c1: /foo/.*
+ plug-attributes:
+ d1: C1
+ deny-auto-connection:
+ slot-attributes:
+ c1: !C1
+ plug-attributes:
+ d1: !D1
+ interface4:
+ allow-connection:
+ plug-attributes:
+ c2: C2
+ slot-attributes:
+ d2: D2
+ deny-connection:
+ plug-snap-id:
+ - snapidsnapidsnapidsnapidsnapid01
+ - snapidsnapidsnapidsnapidsnapid02
+ plug-attributes:
+ c2: !D2
+ slot-attributes:
+ d2: !D2
+ allow-installation:
+ slot-snap-type:
+ - app
+ slot-attributes:
+ e1: E1
+TSLINE
+body-length: 0
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`
+ encoded = strings.Replace(encoded, "TSLINE\n", sds.tsLine, 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.SupportedFormat(), Equals, true)
+ snapDecl := a.(*asserts.SnapDeclaration)
+ c.Check(snapDecl.Series(), Equals, "16")
+ c.Check(snapDecl.SnapID(), Equals, "snap-id-1")
+
+ c.Check(snapDecl.PlugRule("interfaceX"), IsNil)
+ c.Check(snapDecl.SlotRule("interfaceX"), IsNil)
+
+ plugRule1 := snapDecl.PlugRule("interface1")
+ c.Assert(plugRule1, NotNil)
+ c.Assert(plugRule1.DenyInstallation, HasLen, 1)
+ c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes)
+ c.Assert(plugRule1.AllowAutoConnection, HasLen, 1)
+ c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`)
+ c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`)
+ c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"})
+ c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"})
+ c.Assert(plugRule1.DenyAutoConnection, HasLen, 1)
+ c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`)
+ c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`)
+ plugRule2 := snapDecl.PlugRule("interface2")
+ c.Assert(plugRule2, NotNil)
+ c.Assert(plugRule2.AllowInstallation, HasLen, 1)
+ c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Assert(plugRule2.AllowConnection, HasLen, 1)
+ c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`)
+ c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`)
+ c.Assert(plugRule2.DenyConnection, HasLen, 1)
+ c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`)
+ c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`)
+ c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"})
+
+ slotRule3 := snapDecl.SlotRule("interface3")
+ c.Assert(slotRule3, NotNil)
+ c.Assert(slotRule3.DenyInstallation, HasLen, 1)
+ c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes)
+ c.Assert(slotRule3.AllowAutoConnection, HasLen, 1)
+ c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`)
+ c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`)
+ c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"})
+ c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"})
+ c.Assert(slotRule3.DenyAutoConnection, HasLen, 1)
+ c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`)
+ c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`)
+ slotRule4 := snapDecl.SlotRule("interface4")
+ c.Assert(slotRule4, NotNil)
+ c.Assert(slotRule4.AllowAutoConnection, HasLen, 1)
+ c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`)
+ c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`)
+ c.Assert(slotRule4.DenyAutoConnection, HasLen, 1)
+ c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`)
+ c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`)
+ c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"})
+ c.Assert(slotRule4.AllowInstallation, HasLen, 1)
+ c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`)
+ c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"})
+}
+
+func prereqDevAccount(c *C, storeDB assertstest.SignerDB, db *asserts.Database) {
+ dev1Acct := assertstest.NewAccount(storeDB, "developer1", map[string]interface{}{
+ "account-id": "dev-id1",
+ }, "")
+ err := db.Add(dev1Acct)
+ c.Assert(err, IsNil)
+}
+
+func (sds *snapDeclSuite) TestSnapDeclarationCheck(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": "dev-id1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapDecl)
+ c.Assert(err, IsNil)
+}
+
+func (sds *snapDeclSuite) TestSnapDeclarationCheckUntrustedAuthority(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ otherDB := setup3rdPartySigning(c, "other", storeDB, db)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": "dev-id1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := otherDB.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapDecl)
+ c.Assert(err, ErrorMatches, `snap-declaration assertion for "foo" \(id "snap-id-1"\) is not signed by a directly trusted authority:.*`)
+}
+
+func (sds *snapDeclSuite) TestSnapDeclarationCheckMissingPublisherAccount(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": "dev-id1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapDecl)
+ c.Assert(err, ErrorMatches, `snap-declaration assertion for "foo" \(id "snap-id-1"\) does not have a matching account assertion for the publisher "dev-id1"`)
+}
+
+type snapFileDigestSuite struct{}
+
+func (s *snapFileDigestSuite) TestSnapFileSHA3_384(c *C) {
+ exData := []byte("hashmeplease")
+
+ tempdir := c.MkDir()
+ snapFn := filepath.Join(tempdir, "ex.snap")
+ err := ioutil.WriteFile(snapFn, exData, 0644)
+ c.Assert(err, IsNil)
+
+ encDgst, size, err := asserts.SnapFileSHA3_384(snapFn)
+ c.Assert(err, IsNil)
+ c.Check(size, Equals, uint64(len(exData)))
+
+ h3_384 := sha3.Sum384(exData)
+ expected := base64.RawURLEncoding.EncodeToString(h3_384[:])
+ c.Check(encDgst, DeepEquals, expected)
+}
+
+type snapBuildSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+func (sds *snapDeclSuite) TestPrerequisites(c *C) {
+ encoded := "type: snap-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-name: first\n" +
+ "publisher-id: dev-id1\n" +
+ sds.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ prereqs := a.Prerequisites()
+ c.Assert(prereqs, HasLen, 1)
+ c.Check(prereqs[0], DeepEquals, &asserts.Ref{
+ Type: asserts.AccountType,
+ PrimaryKey: []string{"dev-id1"},
+ })
+}
+
+func (sbs *snapBuildSuite) SetUpSuite(c *C) {
+ sbs.ts = time.Now().Truncate(time.Second).UTC()
+ sbs.tsLine = "timestamp: " + sbs.ts.Format(time.RFC3339) + "\n"
+}
+
+const (
+ blobSHA3_384 = "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL"
+)
+
+func (sbs *snapBuildSuite) TestDecodeOK(c *C) {
+ encoded := "type: snap-build\n" +
+ "authority-id: dev-id1\n" +
+ "snap-sha3-384: " + blobSHA3_384 + "\n" +
+ "grade: stable\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-size: 10000\n" +
+ sbs.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SnapBuildType)
+ snapBuild := a.(*asserts.SnapBuild)
+ c.Check(snapBuild.AuthorityID(), Equals, "dev-id1")
+ c.Check(snapBuild.Timestamp(), Equals, sbs.ts)
+ c.Check(snapBuild.SnapID(), Equals, "snap-id-1")
+ c.Check(snapBuild.SnapSHA3_384(), Equals, blobSHA3_384)
+ c.Check(snapBuild.SnapSize(), Equals, uint64(10000))
+ c.Check(snapBuild.Grade(), Equals, "stable")
+}
+
+const (
+ snapBuildErrPrefix = "assertion snap-build: "
+)
+
+func (sbs *snapBuildSuite) TestDecodeInvalid(c *C) {
+ digestHdr := "snap-sha3-384: " + blobSHA3_384 + "\n"
+
+ encoded := "type: snap-build\n" +
+ "authority-id: dev-id1\n" +
+ digestHdr +
+ "grade: stable\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-size: 10000\n" +
+ sbs.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`},
+ {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`},
+ {digestHdr, "", `"snap-sha3-384" header is mandatory`},
+ {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`},
+ {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`},
+ {"snap-size: 10000\n", "", `"snap-size" header is mandatory`},
+ {"snap-size: 10000\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`},
+ {"snap-size: 10000\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`},
+ {"grade: stable\n", "", `"grade" header is mandatory`},
+ {"grade: stable\n", "grade: \n", `"grade" header should not be empty`},
+ {sbs.tsLine, "", `"timestamp" header is mandatory`},
+ {sbs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {sbs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, snapBuildErrPrefix+test.expectedErr)
+ }
+}
+
+func makeStoreAndCheckDB(c *C) (storeDB *assertstest.SigningDB, checkDB *asserts.Database) {
+ trustedPrivKey := testPrivKey0
+ storePrivKey := testPrivKey1
+
+ store := assertstest.NewStoreStack("canonical", trustedPrivKey, storePrivKey)
+ cfg := &asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: store.Trusted,
+ }
+ checkDB, err := asserts.OpenDatabase(cfg)
+ c.Assert(err, IsNil)
+
+ // add store key
+ err = checkDB.Add(store.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ return store.SigningDB, checkDB
+}
+
+func setup3rdPartySigning(c *C, username string, storeDB *assertstest.SigningDB, checkDB *asserts.Database) (signingDB *assertstest.SigningDB) {
+ privKey := testPrivKey2
+
+ acct := assertstest.NewAccount(storeDB, username, map[string]interface{}{
+ "account-id": username,
+ }, "")
+ accKey := assertstest.NewAccountKey(storeDB, acct, nil, privKey.PublicKey(), "")
+
+ err := checkDB.Add(acct)
+ c.Assert(err, IsNil)
+ err = checkDB.Add(accKey)
+ c.Assert(err, IsNil)
+
+ return assertstest.NewSigningDB(acct.AccountID(), privKey)
+}
+
+func (sbs *snapBuildSuite) TestSnapBuildCheck(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+ devDB := setup3rdPartySigning(c, "devel1", storeDB, db)
+
+ headers := map[string]interface{}{
+ "authority-id": "devel1",
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "grade": "devel",
+ "snap-size": "1025",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapBuild, err := devDB.Sign(asserts.SnapBuildType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapBuild)
+ c.Assert(err, IsNil)
+}
+
+func (sbs *snapBuildSuite) TestSnapBuildCheckInconsistentTimestamp(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+ devDB := setup3rdPartySigning(c, "devel1", storeDB, db)
+
+ headers := map[string]interface{}{
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "grade": "devel",
+ "snap-size": "1025",
+ "timestamp": "2013-01-01T14:00:00Z",
+ }
+ snapBuild, err := devDB.Sign(asserts.SnapBuildType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapBuild)
+ c.Assert(err, ErrorMatches, "snap-build assertion timestamp outside of signing key validity")
+}
+
+type snapRevSuite struct {
+ ts time.Time
+ tsLine string
+ validEncoded string
+}
+
+func (srs *snapRevSuite) SetUpSuite(c *C) {
+ srs.ts = time.Now().Truncate(time.Second).UTC()
+ srs.tsLine = "timestamp: " + srs.ts.Format(time.RFC3339) + "\n"
+}
+
+func (srs *snapRevSuite) makeValidEncoded() string {
+ return "type: snap-revision\n" +
+ "authority-id: store-id1\n" +
+ "snap-sha3-384: " + blobSHA3_384 + "\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-size: 123\n" +
+ "snap-revision: 1\n" +
+ "developer-id: dev-id1\n" +
+ "revision: 1\n" +
+ srs.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+}
+
+func (srs *snapRevSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "snap-sha3-384": blobSHA3_384,
+ "snap-id": "snap-id-1",
+ "snap-size": "123",
+ "snap-revision": "1",
+ "developer-id": "dev-id1",
+ "revision": "1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for k, v := range overrides {
+ headers[k] = v
+ }
+ return headers
+}
+
+func (srs *snapRevSuite) TestDecodeOK(c *C) {
+ encoded := srs.makeValidEncoded()
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SnapRevisionType)
+ snapRev := a.(*asserts.SnapRevision)
+ c.Check(snapRev.AuthorityID(), Equals, "store-id1")
+ c.Check(snapRev.Timestamp(), Equals, srs.ts)
+ c.Check(snapRev.SnapID(), Equals, "snap-id-1")
+ c.Check(snapRev.SnapSHA3_384(), Equals, blobSHA3_384)
+ c.Check(snapRev.SnapSize(), Equals, uint64(123))
+ c.Check(snapRev.SnapRevision(), Equals, 1)
+ c.Check(snapRev.DeveloperID(), Equals, "dev-id1")
+ c.Check(snapRev.Revision(), Equals, 1)
+}
+
+const (
+ snapRevErrPrefix = "assertion snap-revision: "
+)
+
+func (srs *snapRevSuite) TestDecodeInvalid(c *C) {
+ encoded := srs.makeValidEncoded()
+
+ digestHdr := "snap-sha3-384: " + blobSHA3_384 + "\n"
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`},
+ {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`},
+ {digestHdr, "", `"snap-sha3-384" header is mandatory`},
+ {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`},
+ {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`},
+ {digestHdr, "snap-sha3-384: eHl6\n", `"snap-sha3-384" header does not have the expected bit length: 24`},
+ {"snap-size: 123\n", "", `"snap-size" header is mandatory`},
+ {"snap-size: 123\n", "snap-size: \n", `"snap-size" header should not be empty`},
+ {"snap-size: 123\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`},
+ {"snap-size: 123\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`},
+ {"snap-revision: 1\n", "", `"snap-revision" header is mandatory`},
+ {"snap-revision: 1\n", "snap-revision: \n", `"snap-revision" header should not be empty`},
+ {"snap-revision: 1\n", "snap-revision: -1\n", `"snap-revision" header must be >=1: -1`},
+ {"snap-revision: 1\n", "snap-revision: 0\n", `"snap-revision" header must be >=1: 0`},
+ {"snap-revision: 1\n", "snap-revision: zzz\n", `"snap-revision" header is not an integer: zzz`},
+ {"developer-id: dev-id1\n", "", `"developer-id" header is mandatory`},
+ {"developer-id: dev-id1\n", "developer-id: \n", `"developer-id" header should not be empty`},
+ {srs.tsLine, "", `"timestamp" header is mandatory`},
+ {srs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {srs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, snapRevErrPrefix+test.expectedErr)
+ }
+}
+
+func prereqSnapDecl(c *C, storeDB assertstest.SignerDB, db *asserts.Database) {
+ snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": "dev-id1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = db.Add(snapDecl)
+ c.Assert(err, IsNil)
+}
+
+func (srs *snapRevSuite) TestSnapRevisionCheck(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+ prereqSnapDecl(c, storeDB, db)
+
+ headers := srs.makeHeaders(nil)
+ snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapRev)
+ c.Assert(err, IsNil)
+}
+
+func (srs *snapRevSuite) TestSnapRevisionCheckInconsistentTimestamp(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ headers := srs.makeHeaders(map[string]interface{}{
+ "timestamp": "2013-01-01T14:00:00Z",
+ })
+ snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapRev)
+ c.Assert(err, ErrorMatches, "snap-revision assertion timestamp outside of signing key validity")
+}
+
+func (srs *snapRevSuite) TestSnapRevisionCheckUntrustedAuthority(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ otherDB := setup3rdPartySigning(c, "other", storeDB, db)
+
+ headers := srs.makeHeaders(nil)
+ snapRev, err := otherDB.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapRev)
+ c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" is not signed by a store:.*`)
+}
+
+func (srs *snapRevSuite) TestSnapRevisionCheckMissingDeveloperAccount(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ headers := srs.makeHeaders(nil)
+ snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapRev)
+ c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching account assertion for the developer "dev-id1"`)
+}
+
+func (srs *snapRevSuite) TestSnapRevisionCheckMissingDeclaration(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+
+ headers := srs.makeHeaders(nil)
+ snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(snapRev)
+ c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching snap-declaration assertion`)
+}
+
+func (srs *snapRevSuite) TestPrimaryKey(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+ prereqSnapDecl(c, storeDB, db)
+
+ headers := srs.makeHeaders(nil)
+ snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = db.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ _, err = db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": headers["snap-sha3-384"].(string),
+ })
+ c.Assert(err, IsNil)
+}
+
+func (srs *snapRevSuite) TestPrerequisites(c *C) {
+ encoded := srs.makeValidEncoded()
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ prereqs := a.Prerequisites()
+ c.Assert(prereqs, HasLen, 2)
+ c.Check(prereqs[0], DeepEquals, &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snap-id-1"},
+ })
+ c.Check(prereqs[1], DeepEquals, &asserts.Ref{
+ Type: asserts.AccountType,
+ PrimaryKey: []string{"dev-id1"},
+ })
+}
+
+type validationSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+func (vs *validationSuite) SetUpSuite(c *C) {
+ vs.ts = time.Now().Truncate(time.Second).UTC()
+ vs.tsLine = "timestamp: " + vs.ts.Format(time.RFC3339) + "\n"
+}
+
+func (vs *validationSuite) makeValidEncoded() string {
+ return "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+}
+
+func (vs *validationSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} {
+ headers := map[string]interface{}{
+ "authority-id": "dev-id1",
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "approved-snap-id": "snap-id-2",
+ "approved-snap-revision": "42",
+ "revision": "1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for k, v := range overrides {
+ headers[k] = v
+ }
+ return headers
+}
+
+func (vs *validationSuite) TestDecodeOK(c *C) {
+ encoded := vs.makeValidEncoded()
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ValidationType)
+ validation := a.(*asserts.Validation)
+ c.Check(validation.AuthorityID(), Equals, "dev-id1")
+ c.Check(validation.Timestamp(), Equals, vs.ts)
+ c.Check(validation.Series(), Equals, "16")
+ c.Check(validation.SnapID(), Equals, "snap-id-1")
+ c.Check(validation.ApprovedSnapID(), Equals, "snap-id-2")
+ c.Check(validation.ApprovedSnapRevision(), Equals, 42)
+ c.Check(validation.Revoked(), Equals, false)
+ c.Check(validation.Revision(), Equals, 1)
+}
+
+const (
+ validationErrPrefix = "assertion validation: "
+)
+
+func (vs *validationSuite) TestDecodeInvalid(c *C) {
+ encoded := vs.makeValidEncoded()
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"series: 16\n", "", `"series" header is mandatory`},
+ {"series: 16\n", "series: \n", `"series" header should not be empty`},
+ {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`},
+ {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`},
+ {"approved-snap-id: snap-id-2\n", "", `"approved-snap-id" header is mandatory`},
+ {"approved-snap-id: snap-id-2\n", "approved-snap-id: \n", `"approved-snap-id" header should not be empty`},
+ {"approved-snap-revision: 42\n", "", `"approved-snap-revision" header is mandatory`},
+ {"approved-snap-revision: 42\n", "approved-snap-revision: z\n", `"approved-snap-revision" header is not an integer: z`},
+ {"approved-snap-revision: 42\n", "approved-snap-revision: 0\n", `"approved-snap-revision" header must be >=1: 0`},
+ {"approved-snap-revision: 42\n", "approved-snap-revision: -1\n", `"approved-snap-revision" header must be >=1: -1`},
+ {vs.tsLine, "", `"timestamp" header is mandatory`},
+ {vs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {vs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, validationErrPrefix+test.expectedErr)
+ }
+}
+
+func prereqSnapDecl2(c *C, storeDB assertstest.SignerDB, db *asserts.Database) {
+ snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-2",
+ "snap-name": "bar",
+ "publisher-id": "dev-id1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = db.Add(snapDecl)
+ c.Assert(err, IsNil)
+}
+
+func (vs *validationSuite) TestValidationCheck(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+ devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db)
+
+ prereqSnapDecl(c, storeDB, db)
+ prereqSnapDecl2(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ validation, err := devDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(validation)
+ c.Assert(err, IsNil)
+}
+
+func (vs *validationSuite) TestValidationCheckWrongAuthority(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+ prereqSnapDecl(c, storeDB, db)
+ prereqSnapDecl2(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ validation, err := storeDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(validation)
+ c.Assert(err, ErrorMatches, `validation assertion by snap "foo" \(id "snap-id-1"\) not signed by its publisher`)
+}
+
+func (vs *validationSuite) TestRevocation(c *C) {
+ encoded := "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revoked: true\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ validation := a.(*asserts.Validation)
+ c.Check(validation.Revoked(), Equals, true)
+}
+
+func (vs *validationSuite) TestRevokedFalse(c *C) {
+ encoded := "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revoked: false\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ validation := a.(*asserts.Validation)
+ c.Check(validation.Revoked(), Equals, false)
+}
+
+func (vs *validationSuite) TestRevokedInvalid(c *C) {
+ encoded := "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revoked: foo\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ _, err := asserts.Decode([]byte(encoded))
+ c.Check(err, ErrorMatches, `.*: "revoked" header must be 'true' or 'false'`)
+}
+
+func (vs *validationSuite) TestMissingGatedSnapDeclaration(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ a, err := storeDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(a)
+ c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion for approved-snap-id "snap-id-2"`)
+}
+
+func (vs *validationSuite) TestMissingGatingSnapDeclaration(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+ prereqSnapDecl2(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ a, err := storeDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(a)
+ c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion`)
+}
+
+func (vs *validationSuite) TestPrerequisites(c *C) {
+ encoded := vs.makeValidEncoded()
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ prereqs := a.Prerequisites()
+ c.Assert(prereqs, HasLen, 2)
+ c.Check(prereqs[0], DeepEquals, &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snap-id-1"},
+ })
+ c.Check(prereqs[1], DeepEquals, &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snap-id-2"},
+ })
+}
+
+type baseDeclSuite struct{}
+
+func (s *baseDeclSuite) TestDecodeOK(c *C) {
+ encoded := `type: base-declaration
+authority-id: canonical
+series: 16
+plugs:
+ interface1:
+ deny-installation: false
+ allow-auto-connection:
+ slot-snap-type:
+ - app
+ slot-publisher-id:
+ - acme
+ slot-attributes:
+ a1: /foo/.*
+ plug-attributes:
+ b1: B1
+ deny-auto-connection:
+ slot-attributes:
+ a1: !A1
+ plug-attributes:
+ b1: !B1
+ interface2:
+ allow-installation: true
+ allow-connection:
+ plug-attributes:
+ a2: A2
+ slot-attributes:
+ b2: B2
+ deny-connection:
+ slot-snap-id:
+ - snapidsnapidsnapidsnapidsnapid01
+ - snapidsnapidsnapidsnapidsnapid02
+ plug-attributes:
+ a2: !A2
+ slot-attributes:
+ b2: !B2
+slots:
+ interface3:
+ deny-installation: false
+ allow-auto-connection:
+ plug-snap-type:
+ - app
+ plug-publisher-id:
+ - acme
+ slot-attributes:
+ c1: /foo/.*
+ plug-attributes:
+ d1: C1
+ deny-auto-connection:
+ slot-attributes:
+ c1: !C1
+ plug-attributes:
+ d1: !D1
+ interface4:
+ allow-connection:
+ plug-attributes:
+ c2: C2
+ slot-attributes:
+ d2: D2
+ deny-connection:
+ plug-snap-id:
+ - snapidsnapidsnapidsnapidsnapid01
+ - snapidsnapidsnapidsnapidsnapid02
+ plug-attributes:
+ c2: !D2
+ slot-attributes:
+ d2: !D2
+ allow-installation:
+ slot-snap-type:
+ - app
+ slot-attributes:
+ e1: E1
+timestamp: 2016-09-29T19:50:49Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ baseDecl := a.(*asserts.BaseDeclaration)
+ c.Check(baseDecl.Series(), Equals, "16")
+ ts, err := time.Parse(time.RFC3339, "2016-09-29T19:50:49Z")
+ c.Assert(err, IsNil)
+ c.Check(baseDecl.Timestamp().Equal(ts), Equals, true)
+
+ c.Check(baseDecl.PlugRule("interfaceX"), IsNil)
+ c.Check(baseDecl.SlotRule("interfaceX"), IsNil)
+
+ plugRule1 := baseDecl.PlugRule("interface1")
+ c.Assert(plugRule1, NotNil)
+ c.Assert(plugRule1.DenyInstallation, HasLen, 1)
+ c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes)
+ c.Assert(plugRule1.AllowAutoConnection, HasLen, 1)
+ c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`)
+ c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`)
+ c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"})
+ c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"})
+ c.Assert(plugRule1.DenyAutoConnection, HasLen, 1)
+ c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`)
+ c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`)
+ plugRule2 := baseDecl.PlugRule("interface2")
+ c.Assert(plugRule2, NotNil)
+ c.Assert(plugRule2.AllowInstallation, HasLen, 1)
+ c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Assert(plugRule2.AllowConnection, HasLen, 1)
+ c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`)
+ c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`)
+ c.Assert(plugRule2.DenyConnection, HasLen, 1)
+ c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`)
+ c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`)
+ c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"})
+
+ slotRule3 := baseDecl.SlotRule("interface3")
+ c.Assert(slotRule3, NotNil)
+ c.Assert(slotRule3.DenyInstallation, HasLen, 1)
+ c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes)
+ c.Assert(slotRule3.AllowAutoConnection, HasLen, 1)
+ c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`)
+ c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`)
+ c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"})
+ c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"})
+ c.Assert(slotRule3.DenyAutoConnection, HasLen, 1)
+ c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`)
+ c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`)
+ slotRule4 := baseDecl.SlotRule("interface4")
+ c.Assert(slotRule4, NotNil)
+ c.Assert(slotRule4.AllowConnection, HasLen, 1)
+ c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`)
+ c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`)
+ c.Assert(slotRule4.DenyConnection, HasLen, 1)
+ c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`)
+ c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`)
+ c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"})
+ c.Assert(slotRule4.AllowInstallation, HasLen, 1)
+ c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`)
+ c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"})
+
+}
+
+func (s *baseDeclSuite) TestBaseDeclarationCheckUntrustedAuthority(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ otherDB := setup3rdPartySigning(c, "other", storeDB, db)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ baseDecl, err := otherDB.Sign(asserts.BaseDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(baseDecl)
+ c.Assert(err, ErrorMatches, `base-declaration assertion for series 16 is not signed by a directly trusted authority: other`)
+}
+
+const (
+ baseDeclErrPrefix = "assertion base-declaration: "
+)
+
+func (s *baseDeclSuite) TestDecodeInvalid(c *C) {
+ tsLine := "timestamp: 2016-09-29T19:50:49Z\n"
+
+ encoded := "type: base-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "plugs:\n interface1: true\n" +
+ "slots:\n interface2: true\n" +
+ tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"series: 16\n", "", `"series" header is mandatory`},
+ {"series: 16\n", "series: \n", `"series" header should not be empty`},
+ {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`},
+ {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`},
+ {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`},
+ {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`},
+ {tsLine, "", `"timestamp" header is mandatory`},
+ {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, baseDeclErrPrefix+test.expectedErr)
+ }
+
+}
+
+func (s *baseDeclSuite) TestBuiltin(c *C) {
+ baseDecl := asserts.BuiltinBaseDeclaration()
+ c.Check(baseDecl, IsNil)
+
+ defer asserts.InitBuiltinBaseDeclaration(nil)
+
+ const headers = `
+type: base-declaration
+authority-id: canonical
+series: 16
+revision: 0
+plugs:
+ network: true
+slots:
+ network:
+ allow-installation:
+ slot-snap-type:
+ - core
+`
+
+ err := asserts.InitBuiltinBaseDeclaration([]byte(headers))
+ c.Assert(err, IsNil)
+
+ baseDecl = asserts.BuiltinBaseDeclaration()
+ c.Assert(baseDecl, NotNil)
+
+ cont, _ := baseDecl.Signature()
+ c.Check(string(cont), Equals, strings.TrimSpace(headers))
+
+ c.Check(baseDecl.AuthorityID(), Equals, "canonical")
+ c.Check(baseDecl.Series(), Equals, "16")
+ c.Check(baseDecl.PlugRule("network").AllowAutoConnection[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes)
+ c.Check(baseDecl.SlotRule("network").AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"core"})
+
+ enc := asserts.Encode(baseDecl)
+ // it's expected that it cannot be decoded
+ _, err = asserts.Decode(enc)
+ c.Check(err, NotNil)
+}
+
+func (s *baseDeclSuite) TestBuiltinInitErrors(c *C) {
+ defer asserts.InitBuiltinBaseDeclaration(nil)
+
+ tests := []struct {
+ headers string
+ err string
+ }{
+ {"", `header entry missing ':' separator: ""`},
+ {"type: foo\n", `the builtin base-declaration "type" header is not set to expected value "base-declaration"`},
+ {"type: base-declaration", `the builtin base-declaration "authority-id" header is not set to expected value "canonical"`},
+ {"type: base-declaration\nauthority-id: canonical", `the builtin base-declaration "series" header is not set to expected value "16"`},
+ {"type: base-declaration\nauthority-id: canonical\nseries: 16\nrevision: zzz", `cannot assemble the builtin-base declaration: "revision" header is not an integer: zzz`},
+ {"type: base-declaration\nauthority-id: canonical\nseries: 16\nplugs: foo", `cannot assemble the builtin base-declaration: "plugs" header must be a map`},
+ }
+
+ for _, t := range tests {
+ err := asserts.InitBuiltinBaseDeclaration([]byte(t.headers))
+ c.Check(err, ErrorMatches, t.err, Commentf(t.headers))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package snapasserts offers helpers to handle snap assertions and their checking for installation.
+package snapasserts
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+func findSnapDeclaration(snapID, name string, db asserts.RODatabase) (*asserts.SnapDeclaration, error) {
+ a, err := db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": release.Series,
+ "snap-id": snapID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot find snap declaration for %q: %s", name, snapID)
+ }
+ snapDecl := a.(*asserts.SnapDeclaration)
+
+ if snapDecl.SnapName() == "" {
+ return nil, fmt.Errorf("cannot install snap %q with a revoked snap declaration", name)
+ }
+
+ return snapDecl, nil
+}
+
+// CrossCheck tries to cross check the name, hash digest and size of a snap plus its metadata in a SideInfo with the relevant snap assertions in a database that should have been populated with them.
+func CrossCheck(name, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db asserts.RODatabase) error {
+ // get relevant assertions and do cross checks
+ a, err := db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": snapSHA3_384,
+ })
+ if err != nil {
+ return fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s", name, snapSHA3_384)
+ }
+ snapRev := a.(*asserts.SnapRevision)
+
+ if snapRev.SnapSize() != snapSize {
+ return fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", name, snapSize, snapRev.SnapSize())
+ }
+
+ snapID := si.SnapID
+
+ if snapRev.SnapID() != snapID || snapRev.SnapRevision() != si.Revision.N {
+ return fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", name, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID())
+ }
+
+ snapDecl, err := findSnapDeclaration(snapID, name, db)
+ if err != nil {
+ return err
+ }
+
+ if snapDecl.SnapName() != name {
+ return fmt.Errorf("cannot install snap %q that is undergoing a rename to %q", name, snapDecl.SnapName())
+ }
+
+ return nil
+}
+
+// DeriveSideInfo tries to construct a SideInfo for the given snap using its digest to find the relevant snap assertions with the information in the given database. It will fail with asserts.ErrNotFound if it cannot find them.
+func DeriveSideInfo(snapPath string, db asserts.RODatabase) (*snap.SideInfo, error) {
+ snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(snapPath)
+ if err != nil {
+ return nil, err
+ }
+
+ // get relevant assertions and reconstruct metadata
+ a, err := db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": snapSHA3_384,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ snapRev := a.(*asserts.SnapRevision)
+
+ if snapRev.SnapSize() != snapSize {
+ return nil, fmt.Errorf("snap %q does not have expected size according to signatures (broken or tampered): %d != %d", snapPath, snapSize, snapRev.SnapSize())
+ }
+
+ snapID := snapRev.SnapID()
+
+ snapDecl, err := findSnapDeclaration(snapID, snapPath, db)
+ if err != nil {
+ return nil, err
+ }
+
+ name := snapDecl.SnapName()
+
+ return &snap.SideInfo{
+ RealName: name,
+ SnapID: snapID,
+ Revision: snap.R(snapRev.SnapRevision()),
+ }, nil
+}
+
+// FetchSnapAssertions fetches the assertions matching the snap file digest using the given fetcher.
+func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384 string) error {
+ // for now starting from the snap-revision will get us all other relevant assertions
+ ref := &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{snapSHA3_384},
+ }
+
+ return f.Fetch(ref)
+}
+
+// FetchSnapDeclaration fetches the snap declaration and its prerequisites for the given snap id using the given fetcher.
+func FetchSnapDeclaration(f asserts.Fetcher, snapID string) error {
+ ref := &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{release.Series, snapID},
+ }
+
+ return f.Fetch(ref)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapasserts_test
+
+import (
+ "crypto"
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "golang.org/x/crypto/sha3"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/snap"
+)
+
+func TestSnapasserts(t *testing.T) { TestingT(t) }
+
+type snapassertsSuite struct {
+ storeSigning *assertstest.StoreStack
+ dev1Acct *asserts.Account
+
+ localDB *asserts.Database
+}
+
+var _ = Suite(&snapassertsSuite{})
+
+func (s *snapassertsSuite) SetUpTest(c *C) {
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+
+ s.dev1Acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+
+ localDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.storeSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+
+ s.localDB = localDB
+
+ // add in prereqs assertions
+ err = s.localDB.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapDecl)
+ c.Assert(err, IsNil)
+}
+
+func fakeSnap(rev int) []byte {
+ fake := fmt.Sprintf("hsqs________________%d", rev)
+ return []byte(fake)
+}
+
+func fakeHash(rev int) []byte {
+ h := sha3.Sum384(fakeSnap(rev))
+ return h[:]
+}
+
+func makeDigest(rev int) string {
+ d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev))
+ if err != nil {
+ panic(err)
+ }
+ return string(d)
+}
+
+func (s *snapassertsSuite) TestCrossCheckHappy(c *C) {
+ digest := makeDigest(12)
+ size := uint64(len(fakeSnap(12)))
+ headers := map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": digest,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-revision": "12",
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ si := &snap.SideInfo{
+ SnapID: "snap-id-1",
+ Revision: snap.R(12),
+ }
+
+ // everything cross checks
+ err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB)
+ c.Check(err, IsNil)
+}
+
+func (s *snapassertsSuite) TestCrossCheckErrors(c *C) {
+ digest := makeDigest(12)
+ size := uint64(len(fakeSnap(12)))
+ headers := map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": digest,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-revision": "12",
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ si := &snap.SideInfo{
+ SnapID: "snap-id-1",
+ Revision: snap.R(12),
+ }
+
+ // different size
+ err = snapasserts.CrossCheck("foo", digest, size+1, si, s.localDB)
+ c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size))
+
+ // mismatched revision vs what we got from store original info
+ err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{
+ SnapID: "snap-id-1",
+ Revision: snap.R(21),
+ }, s.localDB)
+ c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`)
+
+ // mismatched snap id vs what we got from store original info
+ err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{
+ SnapID: "snap-id-other",
+ Revision: snap.R(12),
+ }, s.localDB)
+ c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`)
+
+ // changed name
+ err = snapasserts.CrossCheck("baz", digest, size, si, s.localDB)
+ c.Check(err, ErrorMatches, `cannot install snap "baz" that is undergoing a rename to "foo"`)
+
+}
+
+func (s *snapassertsSuite) TestCrossCheckRevokedSnapDecl(c *C) {
+ // revoked snap declaration (snap-name=="") !
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "revision": "1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapDecl)
+ c.Assert(err, IsNil)
+
+ digest := makeDigest(12)
+ size := uint64(len(fakeSnap(12)))
+ headers = map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": digest,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-revision": "12",
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ si := &snap.SideInfo{
+ SnapID: "snap-id-1",
+ Revision: snap.R(12),
+ }
+
+ err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB)
+ c.Check(err, ErrorMatches, `cannot install snap "foo" with a revoked snap declaration`)
+}
+
+func (s *snapassertsSuite) TestDeriveSideInfoHappy(c *C) {
+ digest := makeDigest(42)
+ size := uint64(len(fakeSnap(42)))
+ headers := map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": digest,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-revision": "42",
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "anon.snap")
+ err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644)
+ c.Assert(err, IsNil)
+
+ si, err := snapasserts.DeriveSideInfo(snapPath, s.localDB)
+ c.Assert(err, IsNil)
+ c.Check(si, DeepEquals, &snap.SideInfo{
+ RealName: "foo",
+ SnapID: "snap-id-1",
+ Revision: snap.R(42),
+ Channel: "",
+ })
+}
+
+func (s *snapassertsSuite) TestDeriveSideInfoNoSignatures(c *C) {
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "anon.snap")
+ err := ioutil.WriteFile(snapPath, fakeSnap(42), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB)
+ // cannot find signatures with metadata for snap
+ c.Assert(err, Equals, asserts.ErrNotFound)
+}
+
+func (s *snapassertsSuite) TestDeriveSideInfoSizeMismatch(c *C) {
+ digest := makeDigest(42)
+ size := uint64(len(fakeSnap(42)))
+ headers := map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": digest,
+ "snap-size": fmt.Sprintf("%d", size+5), // broken
+ "snap-revision": "42",
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "anon.snap")
+ err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB)
+ c.Check(err, ErrorMatches, fmt.Sprintf(`snap %q does not have expected size according to signatures \(broken or tampered\): %d != %d`, snapPath, size, size+5))
+}
+
+func (s *snapassertsSuite) TestDeriveSideInfoRevokedSnapDecl(c *C) {
+ // revoked snap declaration (snap-name=="") !
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "revision": "1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapDecl)
+ c.Assert(err, IsNil)
+
+ digest := makeDigest(42)
+ size := uint64(len(fakeSnap(42)))
+ headers = map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": digest,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-revision": "42",
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.localDB.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "anon.snap")
+ err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB)
+ c.Check(err, ErrorMatches, fmt.Sprintf(`cannot install snap %q with a revoked snap declaration`, snapPath))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build withtestkeys withstagingkeys
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package sysdb
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+const (
+ encodedStagingTrustedAccount = `type: account
+authority-id: canonical
+account-id: canonical
+display-name: Canonical
+timestamp: 2016-04-01T00:00:00.0Z
+username: canonical
+validation: certified
+sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu
+
+AcLBXAQAAQoABgUCV640ggAKCRAHKljtl9kuLrQtEADBji8VwAuislurkFORTmcXV/DOkvyvAYEN
+mB/MLniK4MlLX+RDncDBmF38IK9SRkxbwwJuKgvsjwsYJ3w1P7SGvVfNyU2hLRFtdxDMVC7+A9g3
+N1VW9W+IOWmYeBgXiveqAlSJ9GUvLQiBgUWRBkbyAT6aLkSZrTSjxGRGW/uoNfjj+CbAR4HGbRnn
+IOxDuQyw6rOXQZKfZvkD1NiH+0QzXLv0RivE8+V5uVN+ooUFRoVQmqbj7orvPS9iTY5AMVjCgfo0
+UiWiN6NyCfDBDz0bZhIZlBU4JF5W0I/sEwsuYCxIhFi5uPNmQXqqb5d9Y3bsxIUdMR0+pai1A3eI
+HQmYX12wCnb276R5Adz4iol19oKAR2Qf3VJBvPccdIFU7Qu5FOOihQdMRxULBBXGn1HQF1uW+ue3
+ZQ3x6e8s3XjdDQE/kHCDUkmzhbk1SErgndg6Q1ipKJ+4G6dOc16s66bSFA4QzW53Y40NP0HRWxe2
+tK9VOJ+z9GvGYp5H1ZXbbs78t9bUwL7L6z/eXM6BRho6YY9X7nImpByIkdcV47dCyVFol6NrM5NS
+NSpdtRStGqo7tjPaBf86p2vLOAbwFUuaE3rwf5g/agz4S/v5G5E2tKmfQs6vGYrfVtlOzr8gEoXH
++/hOEC3wYEJjpXmFRjUjJwr0Fbej2TpoITpfzbySpg==
+`
+ encodedStagingRootAccountKey = `type: account-key
+authority-id: canonical
+revision: 3
+public-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu
+account-id: canonical
+name: staging-root
+since: 2016-04-01T00:00:00.0Z
+body-length: 717
+sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu
+
+AcbBTQRWhcGAARAA4wh+b9nyRdZj9gNKuHz8BTNZsLOVv2VJseHBoMNc4aA8EgmLwMF/aP+q1tAQ
+VOeynhfSecIK/2aWKKX+dmU/rfAbnbdHX1NT8OnG2z3qdYdqw1EreN8LcY4DBDfa1RNKcjFvBu+Q
+jxpU289m1yUjjc7yHie84BoYRgDl0icar8KF7vKx44wNhzbca+lw4xGSA5gpDZ1i1smdxdpOSsUY
+WT70ZcJBN1oyKiiiCJUNLwCPzaPsH1i3WwDSaGsbjl8gjf2+LFNFPwdsWRbn3RLlFcFbET2bFe5y
+v6UN+0cSh9qLJeLR2h0WDaVBp5Gx4PAYAfpIIF8EH3YbvI8uuTmBza8Ni0yozOZ2cXCSdezLGW2m
+b6itOq/taBhgl8gzhKqki9jAOWmDBeBIbe2rUuNJrfHVH8+lWTzuzJIcHSHeAjFG1xid+HOOsw0e
+Ag3JMjJaqCGCp0Oc9/WBtHV6jB30jLzht5QjJZ6izIKswRrvt0nCowp74FZ1l1ekXZPhhkA5MBMb
+AoTiz9UvRZAWBPa5gX4R7eaekGjCPWI8NpJ7pT3Xh3NIHIsjyf0JcysoH2V1+A9qT1LOCyczf1Uc
+9d8PXap1zhhQuczZcnw7vAwLEIwldfp08x6klsgiP6jqIB4XKJCjBDu/gn682ydWzfLT8echVHpg
+uI62X67Ns1ZbFWMAEQEAAQ==
+
+AcLBXAQAAQoABgUCV86jSgAKCRAHKljtl9kuLpV6EADO8Q1WKJwoTfeIpBpQfDhdhqJLmW86Qrjq
+P9ZsndN8eA4uSbo08yg9jxi6Q3J/A5QK6rhTz5Nu41frKVpgFr80BpIx8cHsY2dZNyKCm70Jjy4h
+cxteK7mwdAzdWG/Dg7Nr4fhOmpepsh1gIXvjWhTkT226DIO6l45o6N2hMKKkWmqJYqVD6i7UE4Ed
+xmC+IoluhnKGGwM6JpyOw0RViXbLjVDR58n4q1xmK7cFduMoLKszVY4/KGmKT8gA6D4pUOa62F84
+Ejh6akRum7uqygBibYT/DP+KA+MhHvpQ8XZem7IVIEnMJr7U2gde3brbVr0oiCl7FzfiBNy6qw92
+cTsE8o3JV0Lc106SWU28GuWPgyXjoH8imzSmWlpQtlPlKEDwMQt31XDKUKp0ZKiEax3cQ6VjMv1f
+PV3bHNjD+tBq5e1xm/UWyGu7J2N4VPLgUK7F4TPUJk5lwKjmII8KD3KA/IeHnZVN6vmC2nKfhGvw
++rJllQQ0IWY9RfIdzFHpVvthe48g27ok5yEgovAc/s7xWZ6CBgyzYWLQMNFvENj04CzGvxirKwuJ
+Fy5UJIEKB0j0R2qnCz6HZkyQrUsz5HiIIlks18FfOZwuIc4GGPbwwQBoXW7a6KQg0aa62BPj5Iww
+3w60rtTSUsjINkZ/GXLodfzPglOl6VLF7bWx2hGesg==
+`
+)
+
+func init() {
+ stagingTrustedAccount, err := asserts.Decode([]byte(encodedStagingTrustedAccount))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode trusted assertion: %v", err))
+ }
+ stagingRootAccountKey, err := asserts.Decode([]byte(encodedStagingRootAccountKey))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode trusted assertion: %v", err))
+ }
+ trustedStagingAssertions = []asserts.Assertion{stagingTrustedAccount, stagingRootAccountKey}
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package sysdb supports the system-wide assertion database with ways to open it and to manage the trusted set of assertions founding it.
+package sysdb
+
+import (
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/dirs"
+)
+
+func openDatabaseAt(path string, cfg *asserts.DatabaseConfig) (*asserts.Database, error) {
+ bs, err := asserts.OpenFSBackstore(path)
+ if err != nil {
+ return nil, err
+ }
+ keypairMgr, err := asserts.OpenFSKeypairManager(path)
+ if err != nil {
+ return nil, err
+ }
+ cfg.Backstore = bs
+ cfg.KeypairManager = keypairMgr
+ return asserts.OpenDatabase(cfg)
+}
+
+// Open opens the system-wide assertion database with the trusted assertions set configured.
+func Open() (*asserts.Database, error) {
+ cfg := &asserts.DatabaseConfig{
+ Trusted: Trusted(),
+ }
+ return openDatabaseAt(dirs.SnapAssertsDBDir, cfg)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package sysdb_test
+
+import (
+ "os"
+ "path/filepath"
+ "syscall"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+)
+
+func TestSysDB(t *testing.T) { TestingT(t) }
+
+type sysDBSuite struct {
+ extraTrusted []asserts.Assertion
+ probeAssert asserts.Assertion
+}
+
+var _ = Suite(&sysDBSuite{})
+
+func (sdbs *sysDBSuite) SetUpTest(c *C) {
+ tmpdir := c.MkDir()
+
+ pk, _ := assertstest.GenerateKey(752)
+
+ signingDB := assertstest.NewSigningDB("can0nical", pk)
+
+ trustedAcct := assertstest.NewAccount(signingDB, "can0nical", map[string]interface{}{
+ "account-id": "can0nical",
+ "validation": "certified",
+ "timestamp": "2015-11-20T15:04:00Z",
+ }, "")
+
+ trustedAccKey := assertstest.NewAccountKey(signingDB, trustedAcct, map[string]interface{}{
+ "account-id": "can0nical",
+ "since": "2015-11-20T15:04:00Z",
+ "until": "2500-11-20T15:04:00Z",
+ }, pk.PublicKey(), "")
+
+ sdbs.extraTrusted = []asserts.Assertion{trustedAcct, trustedAccKey}
+
+ fakeRoot := filepath.Join(tmpdir, "root")
+ err := os.Mkdir(fakeRoot, os.ModePerm)
+ c.Assert(err, IsNil)
+ dirs.SetRootDir(fakeRoot)
+
+ sdbs.probeAssert = assertstest.NewAccount(signingDB, "probe", nil, "")
+}
+
+func (sdbs *sysDBSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+func (sdbs *sysDBSuite) TestTrusted(c *C) {
+ trusted := sysdb.Trusted()
+ c.Check(trusted, HasLen, 2)
+
+ restore := sysdb.InjectTrusted(sdbs.extraTrusted)
+ defer restore()
+
+ trustedEx := sysdb.Trusted()
+ c.Check(trustedEx, HasLen, 4)
+}
+
+func (sdbs *sysDBSuite) TestOpenSysDatabase(c *C) {
+ db, err := sysdb.Open()
+ c.Assert(err, IsNil)
+ c.Check(db, NotNil)
+
+ // check trusted
+ _, err = db.Find(asserts.AccountKeyType, map[string]string{
+ "account-id": "canonical",
+ "public-key-sha3-384": "-CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk",
+ })
+ c.Assert(err, IsNil)
+
+ trustedAcc, err := db.Find(asserts.AccountType, map[string]string{
+ "account-id": "canonical",
+ })
+ c.Assert(err, IsNil)
+
+ err = db.Check(trustedAcc)
+ c.Check(err, IsNil)
+
+ // extraneous
+ err = db.Check(sdbs.probeAssert)
+ c.Check(err, ErrorMatches, "no matching public key.*")
+}
+
+func (sdbs *sysDBSuite) TestOpenSysDatabaseExtras(c *C) {
+ restore := sysdb.InjectTrusted(sdbs.extraTrusted)
+ defer restore()
+
+ db, err := sysdb.Open()
+ c.Assert(err, IsNil)
+ c.Check(db, NotNil)
+
+ err = db.Check(sdbs.probeAssert)
+ c.Check(err, IsNil)
+}
+
+func (sdbs *sysDBSuite) TestOpenSysDatabaseBackstoreOpenFail(c *C) {
+ // make it not world-writeable
+ oldUmask := syscall.Umask(0)
+ os.MkdirAll(filepath.Join(dirs.SnapAssertsDBDir, "asserts-v0"), 0777)
+ syscall.Umask(oldUmask)
+
+ db, err := sysdb.Open()
+ c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*")
+ c.Check(db, IsNil)
+}
+
+func (sdbs *sysDBSuite) TestOpenSysDatabaseKeypairManagerOpenFail(c *C) {
+ // make it not world-writeable
+ oldUmask := syscall.Umask(0)
+ os.MkdirAll(filepath.Join(dirs.SnapAssertsDBDir, "private-keys-v1"), 0777)
+ syscall.Umask(oldUmask)
+
+ db, err := sysdb.Open()
+ c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*")
+ c.Check(db, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build withtestkeys
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package sysdb
+
+import (
+ "github.com/snapcore/snapd/asserts/systestkeys"
+)
+
+// init will inject the test trusted assertions when this module build tag "withtestkeys" is defined.
+func init() {
+ InjectTrusted(systestkeys.Trusted)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package sysdb
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/osutil"
+)
+
+const (
+ encodedCanonicalAccount = `type: account
+authority-id: canonical
+account-id: canonical
+display-name: Canonical
+timestamp: 2016-04-01T00:00:00.0Z
+username: canonical
+validation: certified
+sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+
+AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw
+TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D
+WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+
+aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY
+oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk
+ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV
+1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps
+1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96
++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P
+k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W
+HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu
+7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5
+Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5
+oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b
+o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1
+MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+
+eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp
+LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs
+WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC
+`
+
+ encodedCanonicalRootAccountKey = `type: account-key
+authority-id: canonical
+revision: 2
+public-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+account-id: canonical
+name: root
+since: 2016-04-01T00:00:00.0Z
+body-length: 1406
+sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+
+AcbDTQRWhcGAASAA4Zdo3CVpKmTecjd3VDBiFbZTKKhcG0UV3FXxyGIe2UsdnJIks4NkVYO+qYk0
+zW26Svpa5OIOJGO2NcgN9bpCYWZOufO1xTmC7jW/fEtqJpX8Kcq20+X5AarqJ5RBVnGLrlz+ZT99
+aHdRZ4YQ2XUZvhbelzWTdK5+2eMSXNrFjO6WwGh9NRekE/NIBNwvULAtJ5nv1KwZaSpZ+klJrstU
+EHPhs+NGGm1Aru01FFl3cWUm5Ao8i9y+pFcPoaRatgtpYU8mg9gP594lvyJqjFofXvHPwztmySqf
+FVAp4gLLfLvRxbXkOfPUz8guidqvg6r4DUD+kCBjKYoT44PjK6l51MzEL2IEy6jdnFTgjHbaYML8
+/5NpuPu8XiSjCpOTeNR+XKzXC2tHRU7j09Xd44vKRhPk0Hc4XsPNBWqfrcbdWmwsFhjfxFDJajOq
+hzWVoiRc5opB5socbRjLf+gYtncxe99oC2FDA2FcftlFoyztho0bAzeFer1IHJIMYWxKMESjvJUE
+pnMMKpIMYY0QfWEo5hXR0TaT+NxW2Z9Jqclgyw13y5iY72ZparHS66J+C7dxCEOswlw1ypNic6MM
+/OzpafIQ10yAT3HeRCJQQOOSSTaold+WpWsQweYCywPcu9S+wCo6CrPzJCCIxOAnXjLYv2ykTJje
+pNJ2+GZ1WH2UeJdJ5sR8fpxxRupqHuEKNRZ+2CqLmFC5kHNszoGolLEvGcK4BJciO4KihnKtxrdX
+dUJIOPBLktA8XiiHSOmLzs2CFjcvlDuPSpe64HIL5yCxO1/GRux4A1Kht1+DqTrL7DjyIW+vIPro
+A1PQwkcAJyScNRxT4bPpUj8geAXWd3n212W+7QVHuQEFezvXC5GbMyR+Xj47FOFcFcSZID1hTZEu
+uMD+AxaBHQKwPfBx1arVKE1OhkuKHeSFtZRP8K8l3qj5W0sIxxIW19W8aziu8ZeDMT+nIEJrJvhx
+zGEdxwCrp3k2/93oDV7g+nb1ZGfIhtmcrKziijghzPLaYaiM9LggqwTARelk3xSzd8+uk3LPXuVl
+fP8/xHApss6sCE3xk4+F3OGbL7HbGuCnoulf795XKLRTy+xU/78piOMNJJQu+G0lMZIO3cZrP6io
+MYDa+jDZw4V4fBRWce/FA3Ot1eIDxCq5v+vfKw+HfUlWcjm6VUQIFZYbK+Lzj6mpXn81BugG3d+M
+0WNFObXIrUbhnKcYkus3TSJ9M1oMEIMp0WfFGAVTd61u36fdi2e+/xbLN0kbYcFRZwd9CmtEeDZ0
+eYx/pvKKaNz/DfUr0piVCRwxuxQ0kVppklHPO4sOTFZUId8KLHg28LbszvupSsHP/nHlW8l5/VK6
+4+KxRV2XofsUnwARAQAB
+
+AcLDXAQAAQoABgUCV83kkgAKCRDUpVvql9g3IA9hIADAkn4VXnJIFblhMSBe6hbTy7z6AfOhZxXR
+Ds/mHsiWfFT6ifGi9SpZowhRX+ff57YvFCjlBqMYLKYE0NsFQYEUc5uBWiFZwC0ENydNhO23DV1B
+elTSs6mr9duPm1eJAozFrQETOD1kz5BIamqBUeaTczjM+9l5i485Ffknbc+EaGOrtMEap0GqjByQ
+u+ykZGvryVQ447avgjvFsMtA0quFi+SoW9PT/9D26e5rD7RIICYWG8mzFRn5Isqs/X4W1uAiKQe9
+pqHMbdNr/FCWX5ws0/nMaOq+b0z4EIIXIfT0JmIlFDQsAgFVnKwYw+zs32cTw4XuzvMhgMDtCowD
+YodhiO/5AOMsMMV0qBsYxbIPJIEz7b6gwTYEJoTVkqTit6o3UgWrAy+p4Y7t0ickYIHgwiuKRS9E
+fu0Ue+32NFp0XFqZElfXLK/U2yjto+fJXu6uAELsXesfFGIOp/nbRbNavUt9jAJeO7ftQczgf39T
+YfA0OKerP5gAOd4+aO3gATPUjfWPsJ9908XC7QqK2BwS1kh/fMrd95mxcmXdF1bBElszKwaToBVQ
+1m52EYp06kkPyOu+fGKFAoIMafcV/2Ztz1WMo/Vp0iP/r0WAtBDw6sDJyWOfRjUEvP7BBdEzraHV
+VblbSrKzhYeEGdMDi6kFC+KEzfPDPFJX1l3saPBkz9VDuESbktyObQp9VfkFKYBgBnw3msQJk+6k
+G4t0o3/DZ7qz/kTJXMogG26Z/FsMhPERsaLTbWRJ3WRyXX8COaTladSf8bG0Oib19outnjuvpjQ0
+qEV9eeGRBlx9mbidSYH95cj0zD2DKpeSZ83M5K1pFg+8RKToGElGTTk8vtdTfDVbmi3+QntfLq+z
+ZMgs2+SmCWrV/MPC04Dl00CXywdKPyf6toomqRP7A5fS7W8P9fdPn+a8JCblcleGj9nvJXBQjue7
+97rofCEszhKhoE9fMCIUcSoTU9YAm5Jr+qclSEbV1pzwTvZ8auMIXtzEZV5n4aK4WPDV+lYCadrL
+DlvJSJRuXRvIMbmvU9b8NxgG8AS88BkX3L9vlOpkMculwG1/iooQvxuFaJDargt370wAQo0lCpG3
+MxnsSusymwnYegvvvr7Xp/KBLZK1+8Djzm3fwAryp4qNo29ciVw3O9lFKmmuiIcxSY0bauXaK6kv
+pTnYkmx7XGPF7Ahb7Ov0/0FE2Lx3JZXSEKeW+VrCcpYQOY++t67b+jf0AV4rZExcLFJzP6MPMimP
+ZCd383NzlzkXK+vAdvTi40HPiM9FYOp6g8JTs5TTdx2/qs/SWFC8AkahIQmH0IpFBJep2JKl2kyr
+FZMvASkHA9bR/UuXDvbMzsUmT/xnERZosQaZgFEO
+`
+)
+
+var (
+ trustedAssertions []asserts.Assertion
+ trustedStagingAssertions []asserts.Assertion
+ trustedExtraAssertions []asserts.Assertion
+)
+
+func init() {
+ canonicalAccount, err := asserts.Decode([]byte(encodedCanonicalAccount))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode trusted assertion: %v", err))
+ }
+ canonicalRootAccountKey, err := asserts.Decode([]byte(encodedCanonicalRootAccountKey))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode trusted assertion: %v", err))
+ }
+ trustedAssertions = []asserts.Assertion{canonicalAccount, canonicalRootAccountKey}
+}
+
+// Trusted returns a copy of the current set of trusted assertions as used by Open.
+func Trusted() []asserts.Assertion {
+ trusted := []asserts.Assertion(nil)
+ if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") {
+ trusted = append(trusted, trustedAssertions...)
+ } else {
+ trusted = append(trusted, trustedStagingAssertions...)
+ }
+ trusted = append(trusted, trustedExtraAssertions...)
+ return trusted
+}
+
+// InjectTrusted injects further assertions into the trusted set for Open.
+// Returns a restore function to reinstate the previous set. Useful
+// for tests or called globally without worrying about restoring.
+func InjectTrusted(extra []asserts.Assertion) (restore func()) {
+ prev := trustedExtraAssertions
+ trustedExtraAssertions = make([]asserts.Assertion, len(prev)+len(extra))
+ copy(trustedExtraAssertions, prev)
+ copy(trustedExtraAssertions[len(prev):], extra)
+ return func() {
+ trustedExtraAssertions = prev
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package systestkeys defines trusted assertions and keys to use in tests.
+package systestkeys
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+const (
+ TestRootPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1
+
+lQcYBAAAAAEBEADx0Loc/418zmw2AIcf5uxC/hgshHyCU98n4cRfJph007X6gXJf
+ifHsKlXlSa5NizsM9WlOgCI3eyekF088q7lQTORDo4YO5x/ZtmcAiePtbMrAac4D
+9j+5Ax24jJ4VniYudQ1wX4x7wtXRpL+lCER0FS5HEQ6L3OW/SntfVtSzoshRO5u7
+r6yYW1t0EE04P7Squ+N/sK+xJytOxCzC2/BwugHgZf3jArpFCuWSZgk9QVmqR1a3
+tynSKrx35OzxSdPyyBa4XOQwKAEquK1Lv/njmYTwATR+zIUa3n7SNyOCz0sOTmBE
+7sSCgUtc+wQF2It1Wazs4YDA8YbTTB8VgveGjg8J8qr6YfSQ6BQDKeUnvHwwJH3Z
+5YSL/KUdeI7SOdFjxSy62szvp4s3jWJSVr/qPkNyxfFAH/HOViRR21e1iufov8NO
+yeLFyW7eiA/OU8QXJXG/S9YiCQotZePYlFG3a6p7crfdO90XQf6bqydlNK2ftVje
+J/1+/LHXj60qHXq5x1BrXPMmhMpOphZf0H5l8Q0YolSeFM/THsKbqWDcRQZrL9vm
+GwDgMGipKG5/83SNUuiN2HGLcKT8ME2WoIPTPLi7O+KeNf5vhrL4soETc3XkCx8S
+RYjDMj7U50OU5Zao7EmQzqWtDmFFDV8dmgKIaMduN4TVEgU7ZMDDa2nJRwARAQAB
+AA/+PAQDZRYR/iNXXRHFd6f/BGN/CXF6W3hIfuP8MmdoWDqBRGKjSc35UpVxSx59
+2bYQGlfAYqDPnTh+Lq4wVs0CCcmDr7vilklLsOOh7dLLVI53RckcvgP8bcU1t6uC
+wrfFHyujAbxdKAxDuCvs+p8yKiNloHK9yv2wscjhFNj+onToxayHKs5fhlLKQGSZ
+XbgF9Yf7XyIxgMTJbVuoBlbC9p9bvt9hY1m2dFNPhgW4DlFtWSMqhR87DHPZ4eHZ
+4srhhTSe2vQHGGKdY4aBUDcd5JyiD1UlO8Ez2ebV0AOqVxlutebC4ujlscQ4OaP9
+LBxCBIaUshgHthtbzI5sepDOMMYJKV0R0+gtW6+rrVaudeSdt62yLF6a8n5m41dP
+6OxGmO84ejoyw/EMutrVeraoz2b5bb35gx9bLEMRFr8XL2x1Ckdx2epNTL9aOVmA
+JiCMGC0zFyt/jbNXnoOjD8tzUj44jrJnY2PcnJHgDogXMoIRduPDnwYaQtXkffkW
+zsVbdUHvMkZuKXUBfsxCwFYgGm2i9y0dGnTSzI03TevRJ1FM2+TN8uQ8h4/C0xfZ
+snXgvVHAwAOJwE8onul8AiepE1ihSWmaQfq/2Hn+0u+wbIsdrpP9xKB88KvZtgVe
+mXj1vbDHw1nbORH63vgzfT8tyIhvR1RfDutQoGKkrZ4ZCIkIAPgDABPYucbnUpv/
+e2OSKd+Z/RGwUqghtp6recs3+9IdIoz/XPQHr9eqmgMUSikRFHLD6s0unIUm1b5s
+Q+98OvadsP0D5EaKjAo0Za2PQVi8Na3eoGDs+DpX2+lhq5lvYCezGNoo50awKhzs
+vRE4RU91bohfNvfJ9bY0AwyrYHDg67Jl/JzWtPNBqfAMlRW5WM9NYvp+Brk8JJLU
++Ncf5w//7S4lH5qBf3rXk6ur8ittIq28MGalW7T8Uk2F7VkrvCDaKkWPP8jwux79
+u1F22ADPYbdHB2RUSv0FGPrOItUyl81V6qTpAqO8iYQVol+B0J95B7Z0DLa+QecH
+vVfaVS8IAPmaokwf3mk36dmbHvDIaPjloD1Gw3PCPZ+dpmGLfvcPm4YcA/uTzbNV
+E46QlTZCny8+5W4xDaetpdODXRvCciwnjJ/wcdpSaMe0R5Res8weIcV2RAM9UNNb
+q6BiTDqyBwk/dmFYY71xus/tuAnxmhZnXrJYjcA1CEsO+cu3SkwYM6dp3d1W0Bfh
+li4b6eT3bC7IRD+KW+3Vdti8bShoLUkK2UwXHhnz0yBBE+8vQc8PoxOwt29EcQDf
+GGL1Tz31yxRF+EADH4SL5ypUZFUctLkJ76WP9vNHqx5Tzrbt2aHqqbtvkxfzcB/m
+k6cm8XzLVxttNHvZkvjwtvl76+X8d2kH/34hjWibosJueZb7HoFuJIoXXtPJ+sY5
+MSnY9+uGW4FgzgyUjWd5bfBCcCOGIqJFj37YVJwPKXaXBr0CzgaeJfLNRqz9Mt6d
+OyqYLdb4ojvFSvhfN7bjAiBbwTbGVsOVVKgiNYudWH5lBS9yqxKyDQeUmwSmgaWa
+Y1zMmK7J/syCqMBlizox3NIjGUsV7JGHzatSGksblTdTHTts3D52yTphonZueYVz
+f27546ta7Fk9uEts8XVrs8YiJgZw8DHEugmuD5ZFb5WrpF96jqpaAuEhUye0fkfA
+GvRP9FpVShfxVockrCrLgCaaDs+/kg7cZS+PDU8uLlXnsKqXvkkH7ip/irQOICh0
+ZXN0cm9vdG9yZymJAjgEEwECACIFAgAAAAECGy8GCwkIBwMCBhUIAgkKCwQWAgMB
+Ah4BAheAAAoJEExxmnn3gXGkIyAQAMmpCPsk3FjfH2wHMxDozPZJmgoPwFBj4VEi
+Qg4pp1pWtTHWPm7qN2bUL0WaJkvdPvvana7T5iGSlQHAjQRgPQfS42+0Nz17AInR
+QbpovdE3S/02UOWaF+VgFrF7IKHQhbxbfmjPBQAr/9mWfe/JGyUqlc14a8IwxOmf
+k4qf3WVj48NI6PdtMYpBKtSpghc7rKQwFLyxEauoBtoF6VLyhha7TFBGGM3LJ5uU
+SPr8oVCybkZ9xbWdfcodbe3Ix/gbG1rvX7Jp/pIlG+7DVKn/0xkR7zPPfDmZOBGd
+VFdg9X8L9+QH00Rverp0cCZ+fN97W13/Mb2/E9Px0y86Omwyhg5SVbikemmybrK8
+JHelbZ2NMmN7YHq2TB1idii30aX/1PN9jGyHHFMWPj2BJmK2aWhN0QSX8sxCoS9O
+NCXwYU5hfRX5RjyWnI51XDhhfpMikqXnLrxzmPme4htaIqMl332MiqusFZ0D6UVw
+Br2jeRhncvRrsscvAibbUWgbN6u70xBGjZZksvT8vkBipkikXWJ8SPm5DBfbRe85
+NnAkj2flf8ZFtNwrCy93JPVqY7j4Ip5AHUqhlUhYyPEMlcPEiNIhqZFUZvMYAIRL
+68Hgqm/HlvtVLR/P7H6mDd7XhVFT5Qxz3f+AD+hmQFf8NN4MDbhCxjkUBsq+eyGG
+97WP6Yv2
+=gJ0v
+-----END PGP PRIVATE KEY BLOCK-----
+`
+
+ encodedTestRootAccount = `type: account
+authority-id: testrootorg
+account-id: testrootorg
+display-name: Testrootorg
+timestamp: 2016-08-11T18:30:57+02:00
+username: testrootorg
+validation: certified
+sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
+
+AcLBUgQAAQoABgUCV6yoQQAAelEQAEdSECpdmV5a2G5VMBzJFuHQUU1FzgZ7gPQjc3l0BibDWm8O
+rDi7IT3L80OkqS2AoQgHS5KtEvKqEmhyfcdzcXgvCkHR5kucRBJJaPy8z6gGMhzZIPlc+EqY+Cvb
+/MQPLvtYYvtAxq1vWz+aDGGwk2Z/dFUG+wofvNWodz400gYTZeFOCZwStBD84S7iY/3pMQgC3+SO
+QMr/VI+bgmOukFqZL0cX4ReiuUs2W45V6EC81UGBjk+k7AVTEXMR1Xo8f0yiRzlLoEdKQMCOC45Q
+n4eedjCToGRPFcktM0QhgfbcpPIQKHNqKGGvtQQXvW5PIZ7AS4rTfQScXTn1dqDsL/ZVdasvOpCP
+5o4WvoWMoU8+Hm4n6ckw4sXn//PZIQrtnkp2DO+9JXXZasIPg4k1mvUQ5Kb9qCcBbaM+OO1izOoC
+3PY8xHNQNfHNHwBMewhnU2NpdTS0mTepN/8iFsDT1vSZ28OE2hgbu1ltqx4AsRkCVyFFx6N6OYm2
+UDNozU9K5w0NY4u9HSTDz4KrBIalAaKY72CIUqeVsmAcYatXglbj7dVTZTw75M0v1thQiSoKFqHw
+CHykZ6BJRgminY1FqOg7tvqTwzYM7lwaE3K8JpAyzie7v+OSLSxy1vlwUmT2lT+h1i28/w+r+R3Q
+C0QC8xuHSvOv3YRtzKna3smAfRlB
+`
+
+ TestRootKeyID = "hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR"
+
+ encodedTestRootAccountKey = `type: account-key
+authority-id: testrootorg
+public-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
+account-id: testrootorg
+name: test-root
+since: 2016-08-11T18:30:57+02:00
+body-length: 717
+sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
+
+AcbBTQRWhcGAARAA8dC6HP+NfM5sNgCHH+bsQv4YLIR8glPfJ+HEXyaYdNO1+oFyX4nx7CpV5Umu
+TYs7DPVpToAiN3snpBdPPKu5UEzkQ6OGDucf2bZnAInj7WzKwGnOA/Y/uQMduIyeFZ4mLnUNcF+M
+e8LV0aS/pQhEdBUuRxEOi9zlv0p7X1bUs6LIUTubu6+smFtbdBBNOD+0qrvjf7CvsScrTsQswtvw
+cLoB4GX94wK6RQrlkmYJPUFZqkdWt7cp0iq8d+Ts8UnT8sgWuFzkMCgBKritS7/545mE8AE0fsyF
+Gt5+0jcjgs9LDk5gRO7EgoFLXPsEBdiLdVms7OGAwPGG00wfFYL3ho4PCfKq+mH0kOgUAynlJ7x8
+MCR92eWEi/ylHXiO0jnRY8UsutrM76eLN41iUla/6j5DcsXxQB/xzlYkUdtXtYrn6L/DTsnixclu
+3ogPzlPEFyVxv0vWIgkKLWXj2JRRt2uqe3K33TvdF0H+m6snZTStn7VY3if9fvyx14+tKh16ucdQ
+a1zzJoTKTqYWX9B+ZfENGKJUnhTP0x7Cm6lg3EUGay/b5hsA4DBoqShuf/N0jVLojdhxi3Ck/DBN
+lqCD0zy4uzvinjX+b4ay+LKBE3N15AsfEkWIwzI+1OdDlOWWqOxJkM6lrQ5hRQ1fHZoCiGjHbjeE
+1RIFO2TAw2tpyUcAEQEAAQ==
+
+AcLBXAQAAQoABgUCV8656QAKCRBMcZp594FxpNWlEADQgBlROdBTHpdZ3/9BbasxenUC3VXusMeK
+0DmnsHrsAsyVk6xiHQQ3hWxvXKWoDkDsOhUqcQTsDBcIaZ18+qwpQciyItd+w3d7SSJ+MKSUpwsB
+NOdgw1ykj7l1M/W7xAAPscFoV1xVSk9+rsLYFYDe23R+ecyotSmF+4QHj5b+hXeVIOUaqQTl5xPC
+h0zVYNIUWv42q4Z+hiBS8+8UJ0G+7z/27XORkGHY6TXCt0aph7s5egr8Lm+/jq7c95HVsa7DwSpv
+SqPajRnlyLiHFXUYAUPEU9oDgPwtLsqUkFfrv1WZ3ja1rDexgKBta+8BRyCAq3gPcMAjhiHXdjoW
+90p893l9N6K82RiEOO9ic0pEezjQldg97oU+ajXNm3ryns+HX6hRd39rpzIsrbVdbCqun4RwMbCM
+EVxgC/cuxMGcS40Co3O8wG3H/WIWOqcRQfolQTexmyzQljYt9WyWJdXmtPtaMzQGbOqE/dIjOK9j
+xvrghVU4kX6fJFwPi+azMrluHV+WGSVxPCuLW8o2aipjOd1/bUQCL5OwRuaEWuLCiV01J8H/JjWV
+hL4gGVqEM2KEPIDwY2yqX36jE7uN9O+mIPnS4Tdj0JQ5ZD1qh34wv+4QvhgNeyP120nuS1ykO9X0
+A806uPC5QK1+cgRMUz8zJ0afDNwE/DvpBQvE5CIi9A==
+`
+
+ TestStorePrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1
+
+lQcYBAAAAAEBEACYmqZm+xLnwg1Oz5RD6N+jzfq8FLm2RT+GTtzSG5l7dKjaBz2R
+om+OSOFnqDTT+QaiJ3DeLZaR0wSn4m29T1m196782f86qRJzcCnUoCaovg6WU9Ug
+jwfr3DbOq+aj49yofRK8cBUSg4LZOhc/TAQecBmxtW7noAqvCkcOmk8Qi9pLqCWu
+wRfUBek54wdktVG1+wEHp2Ute66VrVStIAtEUISNe2peo62jlWj0LynreUsHLX2J
+/Pg6uJYAYGpm8V0i2ajxUg9dIN2AwwcGW7YxI0kdV+jrrKlu6izlCzo+VUBEAIsm
+DOCmUjmwNvNe1XHk71DxgmPPg19TRY5Zg9a+YA1cN4w2LFaha+6LFi+xdobHqZ0P
+seH+CLymuRCZnuDFbUwQ5X0lOECpiOOzZrIZUPvcQjawpjFXASDeIlOhD9wTPc7Z
+TUd2ZiNB9EMmJfcYQ8Fde20Ots8zjZIcSWi6V2Yn4+QkMt2QaYDznFhSgQod0QUi
+SMVK1BzI7kKTI1k3tIeIAjADgOkYyYUnbqZqpXMm6Iu+JyuLYVH+wlpIDbg3wdsa
+d7eBJLtatJBL6Mp7chk9XLrg0Kga+taj8e9N6qwh+KEo8SlebxBW2M2G2RWfdF0h
+SA5o1bIB+dnh1bVNUgBN744cPDZM3IiZOMTTHvmcvoHX9Guf71U/1LCG/wARAQAB
+AA//R+eWwK9NGSa2XowwsE7qEaTcoAKj/t5iMEa4hce7ahBt/02qFRUUu1Zb3xvC
+yJ5uIbmz1PxmFg/4AaMPUkQxYSxzp3CQcnN33izbiPRtQtVKykp2AgFjGh+JM5iL
+9G1Ja5qDWYb2ZuLQpMpaadjHmA/6C2IR/9HJNvEAykCrQIClO0DfgJg7QgwG+N+g
+fDNzbOv4cELPyb6dZKlnXKvcozPNQV0FodI93vZnnacbeXiNgbRNktc/n2uaQlMr
+z5Wq7ODiWdLwqlDyDdnXVYehMUYPDWR+u41/yGNPBB1mNDi3L1OSPTuUHspfpEhA
+JE8ue1DIMwPdQ8oDAJmlmUglxpP1dnR3Q3XqUbsJMT6kAdqc4OSXF+L+E9j7EiA1
+UaXiiK+srj/GWFFdKlSf1JLYX3kOvrH/M1xMB6cmUshuWDfiJUGz9rPhPOIAvK11
++Gog6kV+0JJXBe7oWEf8oewONLg7KtU1sSlHeuECpR+Pi652wXnAMeeHFjeCirp+
+jRPla+oKhrYMfLxk+x2YgMK4usoY6Q/KNTcHNs/FeRpzt50OFIaRbKL/I/CY1pB8
+oakl45D0+c38+6MZVkbPwDRN5ixUJfHwSBwl5qFyF3abP/N0gJVsdfPO0QyDbihm
+1yo5Tvihd7aUkfTAF+E2BkZLIfuY5kREENxY/EHceST20gEIAMOjPOwYkN+V25o+
+MSIj9EBq9xEMpddHilpVXNkRHF2i89CFCUCKcIGe7wROvrqxQSqVrEDET4ZU6iqB
+zsaA5RD4Fia3+eoZjvy4563H54XX2Wp89Qs2T0PREems5UMoeho/kCzSKdnYhhll
+kbekWEqZAOzyCaBjzu7YowjrcUuceUbiDSsh6ds4/goS4h1AO/oroYawZQhvUfaf
+W7ExpOsxuFa7S4N7mLywpeGaWcOuZt3r/EfM4gHpJaEntgqhjfiEtEkfO4dGKiAU
++hg+LmVPyBjQnVhK5NXSBc/zXaXOWqrVEkqTEQcZ5WsmpcB9hzqZIaFw9cAF4PKh
+xm1ZOnkIAMewViBcogHUEzzn9ZxTXKi45po45g5qxsoifNlN3ZfShdrxOjXjYos2
+UujGfN+gZN8vV4bnD3Q6CbpioBT7lTZhweZVRwx/eQa/yQv20ZewL/CJduME8DZj
+rQtyy4MRBhaNf3A8Gvx/CXJZaIHYfldRJYIrq9OuK4ael3Zf0uZwm9AleT5baFz8
+T8iRlojlzhT2+xi+Y/yLCCYFESkxgdXPkhUfYkh/O5NPWxSXnohDgKAtKj4gDe2c
+Qs/zUI5Q+p8qucWbcbASZurDthTD80G6zGYNWX0e/6k45k/tatf0zJGLZVww02uc
+Kq6MVafir1FzkOPxq41zmie8zPTe7zcIAL4m/lnWww+jPxM+LffdtgDqOeRxjgo6
+MV3576MqUakeIGVfnlW7SJCyjN2mnf0JbzrVgv7XxEcZIJrIePutMqdKm1YAt2YR
+1TuU/rsKpUQt+d8t9rWfCYd1xeSn6IdNtoBaMeu6vI13pV1dghPAnQyovUK0xzI6
+seLeVhTU3wG9zZHJBycyE8PDTqE3awEetYLGFkz6DruIjYwylYRPZwSC1xpPcirf
+nkSAeE2U9nmnxDWUQNhWzFTazYr7QQAUzghX3Mf2ZYeoDBBqDg9lQMy2oUJrJtfv
+vqmejP39c3+fJiXlT2k2o0V6B8aZTNVaRn00E3hE+e1Obaa1lV1EWxaDcrQUICh0
+ZXN0cm9vdG9yZyBzdG9yZSmJAjgEEwECACIFAgAAAAECGy8GCwkIBwMCBhUIAgkK
+CwQWAgMBAh4BAheAAAoJEN2glF+93m+NRIEP/2AxZS9tmJ6l7oltpYTEhAQdytAE
+eqahcBYIARSTgvy3YJlOzdKdIoYsGogVvNZ7ashaFCpQtNaNezI7Mhz5cuVoHyYl
+hEctEXSeTNUmxNekdksoBm2QHfnxFHbKLV4Kvj7dlvMhNVbpaMe/qI1SykddGBvh
+woEp2HnHe3lGhlU84+XopEijphI8BXQ2so8bA0jEcuDJOAEXtVzj14miP6nZCsDD
+EKHriukohhCQQUZVm0VOKLfdoi4QuAWbehBmlrhcvRDLvcr6p7jY00803jvaGBjD
+XmS0DT51tNg6W2COQ5xlM9+hjK5n6nyZdT/OYeu+TqtdnpHcZxsF7qKsUBbKeQtA
+Abh0wqtD58Kqp9UTovMVho/+/VEH9+gpfpvrieQvjrpZki2ZVnEhqlINOVwCYH0j
+wC5qKcFeUmHHGhE1ShMKypZvLgqfc0soK8vaz+njN4IYrsWaI0iCQmr6FfV7Q8Ih
+XAcSt/73baWnQsiBWWgl+FOxChDfwEWZaGFgtzyjexLpbi1V+Usuwd0+pX3U/+A6
+uXw5t77PXE4nW73a8EDM2nkG5ru+KswmOC0G7ULB2Cs9UOWqN+XChdii+VC68MMK
+O0gyQlMQf+OPtU18Nff7hfKGY1ZCUbCwvb/+bHBvzpjmtWEuIOwPC0CBgU9G9FcX
+o7ZSZ/h/bUY1EjE2
+=Nc2M
+-----END PGP PRIVATE KEY BLOCK-----
+`
+
+ TestStoreKeyID = "XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y"
+
+ encodedTestStoreAccountKey = `type: account-key
+authority-id: testrootorg
+public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+account-id: testrootorg
+name: test-store
+since: 2016-08-11T18:42:22+02:00
+body-length: 717
+sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
+
+AcbBTQRWhcGAARAAmJqmZvsS58INTs+UQ+jfo836vBS5tkU/hk7c0huZe3So2gc9kaJvjkjhZ6g0
+0/kGoidw3i2WkdMEp+JtvU9Ztfeu/Nn/OqkSc3Ap1KAmqL4OllPVII8H69w2zqvmo+PcqH0SvHAV
+EoOC2ToXP0wEHnAZsbVu56AKrwpHDppPEIvaS6glrsEX1AXpOeMHZLVRtfsBB6dlLXuula1UrSAL
+RFCEjXtqXqOto5Vo9C8p63lLBy19ifz4OriWAGBqZvFdItmo8VIPXSDdgMMHBlu2MSNJHVfo66yp
+buos5Qs6PlVARACLJgzgplI5sDbzXtVx5O9Q8YJjz4NfU0WOWYPWvmANXDeMNixWoWvuixYvsXaG
+x6mdD7Hh/gi8prkQmZ7gxW1MEOV9JThAqYjjs2ayGVD73EI2sKYxVwEg3iJToQ/cEz3O2U1HdmYj
+QfRDJiX3GEPBXXttDrbPM42SHElouldmJ+PkJDLdkGmA85xYUoEKHdEFIkjFStQcyO5CkyNZN7SH
+iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW
+o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B
+1/Rrn+9VP9Swhv8AEQEAAQ==
+
+AcLBXAQAAQoABgUCV866kwAKCRBMcZp594FxpHWHD/9AaZXqyT/Zsmq/VzmAMpd9JvCH4PHQKtAP
+bXfP2Dnpa2wk2wuzQuSWunR8NDRyVh/aNVeTEZ9dFm/B8LR+U2O4rsHmFSeicmsTmo9u/HouRdEU
+zeSc6cbAxMPpfNSjr5J+URLjGRT6oX5fEBmRPx/OC9pEIScMx7uKmTKEnuyMzLRNN/6HiGWKrFCo
+nJdKkwRXrkCHyXWAOv1GumT7NDuyFcjAqt/UdHliTZkDBImKOsBmBVXMUjg7HCSS2uq/5WjStJ+B
+JHQ4GSsXBvVINs6BncNWcvV6mCQ73D57MzGhqo997Zb4tSrn7UNGWK7GLCzV3e/pFlG7pw6HbgnQ
++rxU2Oj/TPVw0tcnUiRl2ttKpm+nua0Cl+MD+Gx0KXLAVp0ZGOQ9yGyP9AePFzcOR8SlRIgxi0EI
+iJkSeYilqoKo3AJhnICRiqvAca2TGJoiJUryEgZ8jbTOElfaF2p+y0xvXGlWbKZm1gzGyvFM5fV5
+hJTlp/am+2uVn6U8wPACir4PrbuXYo7L4MIXww2OEO0ruBIaLARbc5IutSWmw6AEYQUxtsa9bdHV
+Zin7LGbEj6lZm8GycWQwh4B6Vnt6dJRIyPc/9G7uM8Ds/2Wa7+yAxhiPqm8DwlbOYh1npw4X4TLD
+IMGnTv5N3zllI+Xz4rqJzNTzEbvOIcrqWxCedQe79A==
+`
+)
+
+var (
+ TestRootAccount asserts.Assertion
+ TestRootAccountKey asserts.Assertion
+ // here for convenience, does not need to be in the trusted set
+ TestStoreAccountKey asserts.Assertion
+ // Testing-only trusted assertions for injecting in the the system trusted set.
+ Trusted []asserts.Assertion
+)
+
+func init() {
+ acct, err := asserts.Decode([]byte(encodedTestRootAccount))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode trusted assertion: %v", err))
+ }
+ accKey, err := asserts.Decode([]byte(encodedTestRootAccountKey))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode trusted assertion: %v", err))
+ }
+ storeAccKey, err := asserts.Decode([]byte(encodedTestStoreAccountKey))
+ if err != nil {
+ panic(fmt.Sprintf("cannot decode test store assertion: %v", err))
+ }
+
+ TestRootAccount = acct
+ TestRootAccountKey = accKey
+ TestStoreAccountKey = storeAccKey
+ Trusted = []asserts.Assertion{TestRootAccount, TestRootAccountKey}
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "net/mail"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`)
+
+// SystemUser holds a system-user assertion which allows creating local
+// system users.
+type SystemUser struct {
+ assertionBase
+ series []string
+ models []string
+ sshKeys []string
+ since time.Time
+ until time.Time
+}
+
+// BrandID returns the brand identifier that signed this assertion.
+func (su *SystemUser) BrandID() string {
+ return su.HeaderString("brand-id")
+}
+
+// Email returns the email address that this assertion is valid for.
+func (su *SystemUser) Email() string {
+ return su.HeaderString("email")
+}
+
+// Series returns the series that this assertion is valid for.
+func (su *SystemUser) Series() []string {
+ return su.series
+}
+
+// Models returns the models that this assertion is valid for.
+func (su *SystemUser) Models() []string {
+ return su.models
+}
+
+// Name returns the full name of the user (e.g. Random Guy).
+func (su *SystemUser) Name() string {
+ return su.HeaderString("name")
+}
+
+// Username returns the system user name that should be created (e.g. "foo").
+func (su *SystemUser) Username() string {
+ return su.HeaderString("username")
+}
+
+// Password returns the crypt(3) compatible password for the user.
+// Note that only ID: $6$ or stronger is supported (sha512crypt).
+func (su *SystemUser) Password() string {
+ return su.HeaderString("password")
+}
+
+// SSHKeys returns the ssh keys for the user.
+func (su *SystemUser) SSHKeys() []string {
+ return su.sshKeys
+}
+
+// Since returns the time since the assertion is valid.
+func (su *SystemUser) Since() time.Time {
+ return su.since
+}
+
+// Until returns the time until the assertion is valid.
+func (su *SystemUser) Until() time.Time {
+ return su.until
+}
+
+// ValidAt returns whether the system-user is valid at 'when' time.
+func (su *SystemUser) ValidAt(when time.Time) bool {
+ valid := when.After(su.since) || when.Equal(su.since)
+ if valid {
+ valid = when.Before(su.until)
+ }
+ return valid
+}
+
+// Implement further consistency checks.
+func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error {
+ // Do the cross-checks when this assertion is actually used,
+ // i.e. in the create-user code. See also Model.checkConsitency
+
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*SystemUser)(nil)
+
+type shadow struct {
+ ID string
+ Rounds string
+ Salt string
+ Hash string
+}
+
+// crypt(3) compatible hashes have the forms:
+// - $id$salt$hash
+// - $id$rounds=N$salt$hash
+func parseShadowLine(line string) (*shadow, error) {
+ l := strings.SplitN(line, "$", 5)
+ if len(l) != 4 && len(l) != 5 {
+ return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`)
+ }
+
+ // if rounds is the second field, the line must consist of 4
+ if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 {
+ return nil, fmt.Errorf(`missing hash field`)
+ }
+
+ // shadow line without $rounds=N$
+ if len(l) == 4 {
+ return &shadow{
+ ID: l[1],
+ Salt: l[2],
+ Hash: l[3],
+ }, nil
+ }
+ // shadow line with rounds
+ return &shadow{
+ ID: l[1],
+ Rounds: l[2],
+ Salt: l[3],
+ Hash: l[4],
+ }, nil
+}
+
+func checkHashedPassword(headers map[string]interface{}, name string) (string, error) {
+ pw, err := checkOptionalString(headers, name)
+ if err != nil {
+ return "", err
+ }
+ // the pw string is optional, so just return if its empty
+ if pw == "" {
+ return "", nil
+ }
+
+ // parse the shadow line
+ shd, err := parseShadowLine(pw)
+ if err != nil {
+ return "", fmt.Errorf(`%q header invalid: %s`, name, err)
+ }
+
+ // and verify it
+
+ // see crypt(3), ID 6 means SHA-512 (since glibc 2.7)
+ ID, err := strconv.Atoi(shd.ID)
+ if err != nil {
+ return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID)
+ }
+ // double check that we only allow modern hashes
+ if ID < 6 {
+ return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name)
+ }
+
+ // the $rounds=N$ part is optional
+ if strings.HasPrefix(shd.Rounds, "rounds=") {
+ rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1])
+ if err != nil {
+ return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err)
+ }
+ if rounds < 5000 || rounds > 999999999 {
+ return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds)
+ }
+ }
+
+ // see crypt(3) for the legal chars
+ validSaltAndHash := regexp.MustCompile(`^[a-zA-Z0-9./]+$`)
+ if !validSaltAndHash.MatchString(shd.Salt) {
+ return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt)
+ }
+ if !validSaltAndHash.MatchString(shd.Hash) {
+ return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash)
+ }
+
+ return pw, nil
+}
+
+func assembleSystemUser(assert assertionBase) (Assertion, error) {
+ // brand-id here can be different from authority-id,
+ // the code using the assertion must use the policy set
+ // by the model assertion system-user-authority header
+ email, err := checkNotEmptyString(assert.headers, "email")
+ if err != nil {
+ return nil, err
+ }
+ if _, err := mail.ParseAddress(email); err != nil {
+ return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err)
+ }
+
+ series, err := checkStringList(assert.headers, "series")
+ if err != nil {
+ return nil, err
+ }
+ models, err := checkStringList(assert.headers, "models")
+ if err != nil {
+ return nil, err
+ }
+ if _, err := checkOptionalString(assert.headers, "name"); err != nil {
+ return nil, err
+ }
+ if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil {
+ return nil, err
+ }
+ if _, err := checkHashedPassword(assert.headers, "password"); err != nil {
+ return nil, err
+ }
+
+ sshKeys, err := checkStringList(assert.headers, "ssh-keys")
+ if err != nil {
+ return nil, err
+ }
+ since, err := checkRFC3339Date(assert.headers, "since")
+ if err != nil {
+ return nil, err
+ }
+ until, err := checkRFC3339Date(assert.headers, "until")
+ if err != nil {
+ return nil, err
+ }
+ if until.Before(since) {
+ return nil, fmt.Errorf("'until' time cannot be before 'since' time")
+ }
+
+ // "global" system-user assertion can only be valid for 1y
+ if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) {
+ return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified")
+ }
+
+ return &SystemUser{
+ assertionBase: assert,
+ series: series,
+ models: models,
+ sshKeys: sshKeys,
+ since: since,
+ until: until,
+ }, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package asserts_test
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ . "gopkg.in/check.v1"
+)
+
+var (
+ _ = Suite(&systemUserSuite{})
+)
+
+type systemUserSuite struct {
+ until time.Time
+ untilLine string
+ since time.Time
+ sinceLine string
+
+ modelsLine string
+
+ systemUserStr string
+}
+
+const systemUserExample = "type: system-user\n" +
+ "authority-id: canonical\n" +
+ "brand-id: canonical\n" +
+ "email: foo@example.com\n" +
+ "series:\n" +
+ " - 16\n" +
+ "MODELSLINE\n" +
+ "name: Nice Guy\n" +
+ "username: guy\n" +
+ "password: $6$salt$hash\n" +
+ "ssh-keys:\n" +
+ " - ssh-rsa AAAABcdefg\n" +
+ "SINCELINE\n" +
+ "UNTILLINE\n" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (s *systemUserSuite) SetUpTest(c *C) {
+ s.since = time.Now().Truncate(time.Second)
+ s.sinceLine = fmt.Sprintf("since: %s\n", s.since.Format(time.RFC3339))
+ s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second)
+ s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339))
+ s.modelsLine = "models:\n - frobinator\n"
+ s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1)
+ s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1)
+ s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1)
+}
+
+func (s *systemUserSuite) TestDecodeOK(c *C) {
+ a, err := asserts.Decode([]byte(s.systemUserStr))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SystemUserType)
+ systemUser := a.(*asserts.SystemUser)
+ c.Check(systemUser.BrandID(), Equals, "canonical")
+ c.Check(systemUser.Email(), Equals, "foo@example.com")
+ c.Check(systemUser.Series(), DeepEquals, []string{"16"})
+ c.Check(systemUser.Models(), DeepEquals, []string{"frobinator"})
+ c.Check(systemUser.Name(), Equals, "Nice Guy")
+ c.Check(systemUser.Username(), Equals, "guy")
+ c.Check(systemUser.Password(), Equals, "$6$salt$hash")
+ c.Check(systemUser.SSHKeys(), DeepEquals, []string{"ssh-rsa AAAABcdefg"})
+ c.Check(systemUser.Since().Equal(s.since), Equals, true)
+ c.Check(systemUser.Until().Equal(s.until), Equals, true)
+}
+
+func (s *systemUserSuite) TestDecodePasswd(c *C) {
+ validTests := []struct{ original, valid string }{
+ {"password: $6$salt$hash\n", "password: $6$rounds=9999$salt$hash\n"},
+ {"password: $6$salt$hash\n", ""},
+ }
+ for _, test := range validTests {
+ valid := strings.Replace(s.systemUserStr, test.original, test.valid, 1)
+ _, err := asserts.Decode([]byte(valid))
+ c.Check(err, IsNil)
+ }
+}
+
+func (s *systemUserSuite) TestValidAt(c *C) {
+ a, err := asserts.Decode([]byte(s.systemUserStr))
+ c.Assert(err, IsNil)
+ su := a.(*asserts.SystemUser)
+
+ c.Check(su.ValidAt(su.Since()), Equals, true)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, true)
+
+ c.Check(su.ValidAt(su.Until()), Equals, false)
+ c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, true)
+ c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false)
+}
+
+func (s *systemUserSuite) TestValidAtRevoked(c *C) {
+ // With since == until, i.e. system-user has been revoked.
+ revoked := strings.Replace(s.systemUserStr, s.sinceLine, fmt.Sprintf("since: %s\n", s.until.Format(time.RFC3339)), 1)
+ a, err := asserts.Decode([]byte(revoked))
+ c.Assert(err, IsNil)
+ su := a.(*asserts.SystemUser)
+
+ c.Check(su.ValidAt(su.Since()), Equals, false)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, false)
+
+ c.Check(su.ValidAt(su.Until()), Equals, false)
+ c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, false)
+ c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false)
+}
+
+const (
+ systemUserErrPrefix = "assertion system-user: "
+)
+
+func (s *systemUserSuite) TestDecodeInvalid(c *C) {
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: canonical\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: canonical\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"email: foo@example.com\n", "", `"email" header is mandatory`},
+ {"email: foo@example.com\n", "email: \n", `"email" header should not be empty`},
+ {"email: foo@example.com\n", "email: <alice!example.com>\n", `"email" header must be a RFC 5322 compliant email address: mail: missing @ in addr-spec`},
+ {"email: foo@example.com\n", "email: no-mail\n", `"email" header must be a RFC 5322 compliant email address:.*`},
+ {"series:\n - 16\n", "series: \n", `"series" header must be a list of strings`},
+ {"series:\n - 16\n", "series: something\n", `"series" header must be a list of strings`},
+ {"models:\n - frobinator\n", "models: \n", `"models" header must be a list of strings`},
+ {"models:\n - frobinator\n", "models: something\n", `"models" header must be a list of strings`},
+ {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: \n", `"ssh-keys" header must be a list of strings`},
+ {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: something\n", `"ssh-keys" header must be a list of strings`},
+ {"name: Nice Guy\n", "name:\n - foo\n", `"name" header must be a string`},
+ {"username: guy\n", "username:\n - foo\n", `"username" header must be a string`},
+ {"username: guy\n", "username: bäää\n", `"username" header contains invalid characters: "bäää"`},
+ {"username: guy\n", "", `"username" header is mandatory`},
+ {"password: $6$salt$hash\n", "password:\n - foo\n", `"password" header must be a string`},
+ {"password: $6$salt$hash\n", "password: cleartext\n", `"password" header invalid: hashed password must be of the form "\$integer-id\$salt\$hash", see crypt\(3\)`},
+ {"password: $6$salt$hash\n", "password: $ni!$salt$hash\n", `"password" header must start with "\$integer-id\$", got "ni!"`},
+ {"password: $6$salt$hash\n", "password: $3$salt$hash\n", `"password" header only supports \$id\$ values of 6 \(sha512crypt\) or higher`},
+ {"password: $6$salt$hash\n", "password: $7$invalid-salt$hash\n", `"password" header has invalid chars in salt "invalid-salt"`},
+ {"password: $6$salt$hash\n", "password: $8$salt$invalid-hash\n", `"password" header has invalid chars in hash "invalid-hash"`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=9999$hash\n", `"password" header invalid: missing hash field`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=xxx$salt$hash\n", `"password" header has invalid number of rounds:.*`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=1$salt$hash\n", `"password" header rounds parameter out of bounds: 1`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=1999999999$salt$hash\n", `"password" header rounds parameter out of bounds: 1999999999`},
+ {s.sinceLine, "since: \n", `"since" header should not be empty`},
+ {s.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`},
+ {s.untilLine, "until: \n", `"until" header should not be empty`},
+ {s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`},
+ {s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr)
+ }
+}
+
+func (s *systemUserSuite) TestUntilNoModels(c *C) {
+ // no models is good for <1y
+ su := strings.Replace(s.systemUserStr, s.modelsLine, "", -1)
+ _, err := asserts.Decode([]byte(su))
+ c.Check(err, IsNil)
+
+ // but invalid for more than one year
+ oneYearPlusOne := time.Now().AddDate(1, 0, 1).Truncate(time.Second)
+ su = strings.Replace(su, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1)
+ _, err = asserts.Decode([]byte(su))
+ c.Check(err, ErrorMatches, systemUserErrPrefix+"'until' time cannot be more than 365 days in the future when no models are specified")
+}
+
+func (s *systemUserSuite) TestUntilWithModels(c *C) {
+ // with models it can be valid forever
+ oneYearPlusOne := time.Now().AddDate(10, 0, 1).Truncate(time.Second)
+ su := strings.Replace(s.systemUserStr, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1)
+ _, err := asserts.Decode([]byte(su))
+ c.Check(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package boottest
+
+import (
+ "path/filepath"
+)
+
+// MockBootloader mocks the bootloader interface and records all
+// set/get calls.
+type MockBootloader struct {
+ BootVars map[string]string
+ SetErr error
+ GetErr error
+
+ name string
+ bootdir string
+}
+
+func NewMockBootloader(name, bootdir string) *MockBootloader {
+ return &MockBootloader{
+ name: name,
+ bootdir: bootdir,
+
+ BootVars: make(map[string]string),
+ }
+}
+
+func (b *MockBootloader) SetBootVars(values map[string]string) error {
+ for k, v := range values {
+ b.BootVars[k] = v
+ }
+ return b.SetErr
+}
+
+func (b *MockBootloader) GetBootVars(keys ...string) (map[string]string, error) {
+ out := map[string]string{}
+ for _, k := range keys {
+ out[k] = b.BootVars[k]
+ }
+
+ return out, b.GetErr
+}
+
+func (b *MockBootloader) Dir() string {
+ return b.bootdir
+}
+
+func (b *MockBootloader) Name() string {
+ return b.name
+}
+
+func (b *MockBootloader) ConfigFile() string {
+ return filepath.Join(b.bootdir, "mockboot/mockboot.cfg")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package boot
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+// RemoveKernelAssets removes the unpacked kernel/initrd for the given
+// kernel snap.
+func RemoveKernelAssets(s snap.PlaceInfo) error {
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ return fmt.Errorf("no not remove kernel assets: %s", err)
+ }
+
+ // remove the kernel blob
+ blobName := filepath.Base(s.MountFile())
+ dstDir := filepath.Join(bootloader.Dir(), blobName)
+ if err := os.RemoveAll(dstDir); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func copyAll(src, dst string) error {
+ if output, err := exec.Command("cp", "-aLv", src, dst).CombinedOutput(); err != nil {
+ return fmt.Errorf("cannot copy %q -> %q: %s (%s)", src, dst, err, output)
+ }
+ return nil
+}
+
+// ExtractKernelAssets extracts kernel/initrd/dtb data from the given
+// kernel snap, if required, to a versioned bootloader directory so
+// that the bootloader can use it.
+func ExtractKernelAssets(s *snap.Info, snapf snap.Container) error {
+ if s.Type != snap.TypeKernel {
+ return fmt.Errorf("cannot extract kernel assets from snap type %q", s.Type)
+ }
+
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ return fmt.Errorf("cannot extract kernel assets: %s", err)
+ }
+
+ if bootloader.Name() == "grub" {
+ return nil
+ }
+
+ // now do the kernel specific bits
+ blobName := filepath.Base(s.MountFile())
+ dstDir := filepath.Join(bootloader.Dir(), blobName)
+ if err := os.MkdirAll(dstDir, 0755); err != nil {
+ return err
+ }
+ dir, err := os.Open(dstDir)
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+
+ for _, src := range []string{"kernel.img", "initrd.img"} {
+ if err := snapf.Unpack(src, dstDir); err != nil {
+ return err
+ }
+ if err := dir.Sync(); err != nil {
+ return err
+ }
+ }
+ if err := snapf.Unpack("dtbs/*", dstDir); err != nil {
+ return err
+ }
+
+ return dir.Sync()
+}
+
+// SetNextBoot will schedule the given OS or kernel snap to be used in
+// the next boot
+func SetNextBoot(s *snap.Info) error {
+ if release.OnClassic {
+ return nil
+ }
+ if s.Type != snap.TypeOS && s.Type != snap.TypeKernel {
+ return nil
+ }
+
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ return fmt.Errorf("cannot set next boot: %s", err)
+ }
+
+ var nextBoot, goodBoot string
+ switch s.Type {
+ case snap.TypeOS:
+ nextBoot = "snap_try_core"
+ goodBoot = "snap_core"
+ case snap.TypeKernel:
+ nextBoot = "snap_try_kernel"
+ goodBoot = "snap_kernel"
+ }
+ blobName := filepath.Base(s.MountFile())
+
+ // check if we actually need to do anything, i.e. the exact same
+ // kernel/core revision got installed again (e.g. firstboot)
+ m, err := bootloader.GetBootVars(goodBoot)
+ if err != nil {
+ return err
+ }
+ if m[goodBoot] == blobName {
+ return nil
+ }
+
+ return bootloader.SetBootVars(map[string]string{
+ nextBoot: blobName,
+ "snap_mode": "try",
+ })
+}
+
+// KernelOrOsRebootRequired returns whether a reboot is required to swith to the given OS or kernel snap.
+func KernelOrOsRebootRequired(s *snap.Info) bool {
+ if s.Type != snap.TypeKernel && s.Type != snap.TypeOS {
+ return false
+ }
+
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ logger.Noticef("cannot get boot settings: %s", err)
+ return false
+ }
+
+ var nextBoot, goodBoot string
+ switch s.Type {
+ case snap.TypeKernel:
+ nextBoot = "snap_try_kernel"
+ goodBoot = "snap_kernel"
+ case snap.TypeOS:
+ nextBoot = "snap_try_core"
+ goodBoot = "snap_core"
+ }
+
+ m, err := bootloader.GetBootVars(nextBoot, goodBoot)
+ if err != nil {
+ return false
+ }
+
+ squashfsName := filepath.Base(s.MountFile())
+ if m[nextBoot] == squashfsName && m[goodBoot] != m[nextBoot] {
+ return true
+ }
+
+ return false
+}
+
+// InUse checks if the given name/revision is used in the
+// boot environment
+func InUse(name string, rev snap.Revision) bool {
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ logger.Noticef("cannot get boot settings: %s", err)
+ return false
+ }
+
+ bootVars, err := bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_core", "snap_try_core")
+ if err != nil {
+ logger.Noticef("cannot get boot vars: %s", err)
+ return false
+ }
+
+ snapFile := filepath.Base(snap.MountFile(name, rev))
+ for _, bootVar := range bootVars {
+ if bootVar == snapFile {
+ return true
+ }
+ }
+
+ return false
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package boot_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+func TestBoot(t *testing.T) { TestingT(t) }
+
+type kernelOSSuite struct {
+ bootloader *boottest.MockBootloader
+}
+
+var _ = Suite(&kernelOSSuite{})
+
+func (s *kernelOSSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ s.bootloader = boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(s.bootloader)
+}
+
+func (s *kernelOSSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ partition.ForceBootloader(nil)
+}
+
+const packageKernel = `
+name: ubuntu-kernel
+version: 4.0-1
+type: kernel
+vendor: Someone
+`
+
+func (s *kernelOSSuite) TestExtractKernelAssetsAndRemove(c *C) {
+ files := [][]string{
+ {"kernel.img", "I'm a kernel"},
+ {"initrd.img", "...and I'm an initrd"},
+ {"dtbs/foo.dtb", "g'day, I'm foo.dtb"},
+ {"dtbs/bar.dtb", "hello, I'm bar.dtb"},
+ // must be last
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+
+ si := &snap.SideInfo{
+ RealName: "ubuntu-kernel",
+ Revision: snap.R(42),
+ }
+ fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+
+ err = boot.ExtractKernelAssets(info, snapf)
+ c.Assert(err, IsNil)
+
+ // this is where the kernel/initrd is unpacked
+ bootdir := s.bootloader.Dir()
+
+ kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap")
+
+ for _, def := range files {
+ if def[0] == "meta/kernel.yaml" {
+ break
+ }
+
+ fullFn := filepath.Join(kernelAssetsDir, def[0])
+ content, err := ioutil.ReadFile(fullFn)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, def[1])
+ }
+
+ // remove
+ err = boot.RemoveKernelAssets(info)
+ c.Assert(err, IsNil)
+
+ c.Check(osutil.FileExists(kernelAssetsDir), Equals, false)
+}
+
+func (s *kernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) {
+ // pretend to be a grub system
+ mockGrub := boottest.NewMockBootloader("grub", c.MkDir())
+ partition.ForceBootloader(mockGrub)
+
+ files := [][]string{
+ {"kernel.img", "I'm a kernel"},
+ {"initrd.img", "...and I'm an initrd"},
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ si := &snap.SideInfo{
+ RealName: "ubuntu-kernel",
+ Revision: snap.R(42),
+ }
+ fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+
+ err = boot.ExtractKernelAssets(info, snapf)
+ c.Assert(err, IsNil)
+
+ // kernel is *not* here
+ kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ c.Assert(osutil.FileExists(kernimg), Equals, false)
+}
+
+func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) {
+ info := &snap.Info{}
+ info.Type = snap.TypeApp
+
+ err := boot.ExtractKernelAssets(info, nil)
+ c.Assert(err, ErrorMatches, `cannot extract kernel assets from snap type "app"`)
+}
+
+// SetNextBoot should do nothing on classic LP: #1580403
+func (s *kernelOSSuite) TestSetNextBootOnClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ // Create a fake OS snap that we try to update
+ snapInfo := snaptest.MockSnap(c, "name: os\ntype: os", "SNAP", &snap.SideInfo{Revision: snap.R(42)})
+ err := boot.SetNextBoot(snapInfo)
+ c.Assert(err, IsNil)
+
+ c.Assert(s.bootloader.BootVars, HasLen, 0)
+}
+
+func (s *kernelOSSuite) TestSetNextBootForCore(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ info := &snap.Info{}
+ info.Type = snap.TypeOS
+ info.RealName = "core"
+ info.Revision = snap.R(100)
+
+ err := boot.SetNextBoot(info)
+ c.Assert(err, IsNil)
+
+ c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{
+ "snap_try_core": "core_100.snap",
+ "snap_mode": "try",
+ })
+
+ c.Check(boot.KernelOrOsRebootRequired(info), Equals, true)
+}
+
+func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ info := &snap.Info{}
+ info.Type = snap.TypeKernel
+ info.RealName = "krnl"
+ info.Revision = snap.R(42)
+
+ err := boot.SetNextBoot(info)
+ c.Assert(err, IsNil)
+
+ c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{
+ "snap_try_kernel": "krnl_42.snap",
+ "snap_mode": "try",
+ })
+
+ s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap"
+ s.bootloader.BootVars["snap_try_kernel"] = "krnl_42.snap"
+ c.Check(boot.KernelOrOsRebootRequired(info), Equals, true)
+
+ // simulate good boot
+ s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap"
+ c.Check(boot.KernelOrOsRebootRequired(info), Equals, false)
+}
+
+func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ info := &snap.Info{}
+ info.Type = snap.TypeKernel
+ info.RealName = "krnl"
+ info.Revision = snap.R(40)
+
+ s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap"
+
+ err := boot.SetNextBoot(info)
+ c.Assert(err, IsNil)
+
+ c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{
+ "snap_kernel": "krnl_40.snap",
+ })
+}
+
+func (s *kernelOSSuite) TestInUse(c *C) {
+ for _, t := range []struct {
+ bootVarKey string
+ bootVarValue string
+
+ snapName string
+ snapRev snap.Revision
+
+ inUse bool
+ }{
+ // in use
+ {"snap_kernel", "kernel_41.snap", "kernel", snap.R(41), true},
+ {"snap_try_kernel", "kernel_82.snap", "kernel", snap.R(82), true},
+ {"snap_core", "core_21.snap", "core", snap.R(21), true},
+ {"snap_try_core", "core_42.snap", "core", snap.R(42), true},
+ // not in use
+ {"snap_core", "core_111.snap", "core", snap.R(21), false},
+ {"snap_try_core", "core_111.snap", "core", snap.R(21), false},
+ {"snap_kernel", "kernel_111.snap", "kernel", snap.R(1), false},
+ {"snap_try_kernel", "kernel_111.snap", "kernel", snap.R(1), false},
+ } {
+ s.bootloader.BootVars[t.bootVarKey] = t.bootVarValue
+ c.Assert(boot.InUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+// aliasAction represents an action performed on aliases.
+type aliasAction struct {
+ Action string `json:"action"`
+ Snap string `json:"snap"`
+ Aliases []string `json:"aliases"`
+}
+
+// performAliasAction performs a single action on aliases.
+func (client *Client) performAliasAction(sa *aliasAction) (changeID string, err error) {
+ b, err := json.Marshal(sa)
+ if err != nil {
+ return "", err
+ }
+ return client.doAsync("POST", "/v2/aliases", nil, nil, bytes.NewReader(b))
+}
+
+// Alias enables the provided aliases for the snap with snapName.
+func (client *Client) Alias(snapName string, aliases []string) (changeID string, err error) {
+ return client.performAliasAction(&aliasAction{
+ Action: "alias",
+ Snap: snapName,
+ Aliases: aliases,
+ })
+}
+
+// Unalias disables explicitly the provided aliases for the snap with snapName.
+func (client *Client) Unalias(snapName string, aliases []string) (changeID string, err error) {
+ return client.performAliasAction(&aliasAction{
+ Action: "unalias",
+ Snap: snapName,
+ Aliases: aliases,
+ })
+}
+
+// ResetAliases resets the provided aliases for the snap with snapName
+// to their default state, enabled for auto-aliases, disabled otherwise.
+func (client *Client) ResetAliases(snapName string, aliases []string) (changeID string, err error) {
+ return client.performAliasAction(&aliasAction{
+ Action: "reset",
+ Snap: snapName,
+ Aliases: aliases,
+ })
+}
+
+// AliasStatus represents the status of an alias.
+type AliasStatus struct {
+ App string `json:"app,omitempty"`
+ Status string `json:"status,omitempty"`
+}
+
+// Aliases returns a map snap -> alias -> AliasStatus for all snaps and aliases in the system.
+func (client *Client) Aliases() (allStatuses map[string]map[string]AliasStatus, err error) {
+ _, err = client.doSync("GET", "/v2/aliases", nil, nil, nil, &allStatuses)
+ return
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+func (cs *clientSuite) TestClientAliasCallsEndpoint(c *check.C) {
+ cs.cli.Alias("alias-snap", []string{"alias1", "alias2"})
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases")
+}
+
+func (cs *clientSuite) TestClientAlias(c *check.C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "chgid"
+ }`
+ id, err := cs.cli.Alias("alias-snap", []string{"alias1", "alias2"})
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "chgid")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "action": "alias",
+ "snap": "alias-snap",
+ "aliases": []interface{}{"alias1", "alias2"},
+ })
+}
+
+func (cs *clientSuite) TestClientUnaliasCallsEndpoint(c *check.C) {
+ cs.cli.Unalias("alias-snap", []string{"alias1", "alias2"})
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases")
+}
+
+func (cs *clientSuite) TestClientUnalias(c *check.C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "chgid"
+ }`
+ id, err := cs.cli.Unalias("alias-snap", []string{"alias1", "alias2"})
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "chgid")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "action": "unalias",
+ "snap": "alias-snap",
+ "aliases": []interface{}{"alias1", "alias2"},
+ })
+}
+
+func (cs *clientSuite) TestClientRestAliasesCallsEndpoint(c *check.C) {
+ cs.cli.ResetAliases("alias-snap", []string{"alias1", "alias2"})
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases")
+}
+
+func (cs *clientSuite) TestClientResetAliases(c *check.C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "chgid"
+ }`
+ id, err := cs.cli.ResetAliases("alias-snap", []string{"alias1", "alias2"})
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "chgid")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "action": "reset",
+ "snap": "alias-snap",
+ "aliases": []interface{}{"alias1", "alias2"},
+ })
+}
+
+func (cs *clientSuite) TestClientAliasesCallsEndpoint(c *check.C) {
+ _, _ = cs.cli.Aliases()
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases")
+}
+
+func (cs *clientSuite) TestClientAliases(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "foo": {
+ "foo0": {"app": "foo", "status": "auto"},
+ "foo_reset": {"app": "foo.reset"}
+ },
+ "bar": {
+ "bar_dump": {"app": "bar.dump", "status": "enabled"},
+ "bar_dump.1": {"status": "disabled"}
+ }
+
+ }
+ }`
+ allStatuses, err := cs.cli.Aliases()
+ c.Assert(err, check.IsNil)
+ c.Check(allStatuses, check.DeepEquals, map[string]map[string]client.AliasStatus{
+ "foo": {
+ "foo0": {App: "foo", Status: "auto"},
+ "foo_reset": {App: "foo.reset", Status: ""},
+ },
+ "bar": {
+ "bar_dump": {App: "bar.dump", Status: "enabled"},
+ "bar_dump.1": {App: "", Status: "disabled"},
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+
+ "github.com/snapcore/snapd/asserts" // for parsing
+)
+
+// Ack tries to add an assertion to the system assertion
+// database. To succeed the assertion must be valid, its signature
+// verified with a known public key and the assertion consistent with
+// and its prerequisite in the database.
+func (client *Client) Ack(b []byte) error {
+ var rsp interface{}
+ if _, err := client.doSync("POST", "/v2/assertions", nil, nil, bytes.NewReader(b), &rsp); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Known queries assertions with type assertTypeName and matching assertion headers.
+func (client *Client) Known(assertTypeName string, headers map[string]string) ([]asserts.Assertion, error) {
+ path := fmt.Sprintf("/v2/assertions/%s", assertTypeName)
+ q := url.Values{}
+
+ if len(headers) > 0 {
+ for k, v := range headers {
+ q.Set(k, v)
+ }
+ }
+
+ response, err := client.raw("GET", path, q, nil, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query assertions: %v", err)
+ }
+ defer response.Body.Close()
+ if response.StatusCode != http.StatusOK {
+ return nil, parseError(response)
+ }
+
+ sanityCount, err := strconv.Atoi(response.Header.Get("X-Ubuntu-Assertions-Count"))
+ if err != nil {
+ return nil, fmt.Errorf("invalid assertions count")
+ }
+
+ dec := asserts.NewDecoder(response.Body)
+
+ asserts := []asserts.Assertion{}
+
+ // TODO: make sure asserts can decode and deal with unknown types
+ for {
+ a, err := dec.Decode()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode assertions: %v", err)
+ }
+ asserts = append(asserts, a)
+ }
+
+ if len(asserts) != sanityCount {
+ return nil, fmt.Errorf("response did not have the expected number of assertions")
+ }
+
+ return asserts, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "errors"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+func (cs *clientSuite) TestClientAssert(c *C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {}
+ }`
+ a := []byte("Assertion.")
+ err := cs.cli.Ack(a)
+ c.Assert(err, IsNil)
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ c.Check(body, DeepEquals, a)
+ c.Check(cs.req.Method, Equals, "POST")
+ c.Check(cs.req.URL.Path, Equals, "/v2/assertions")
+}
+
+func (cs *clientSuite) TestClientAssertsCallsEndpoint(c *C) {
+ _, _ = cs.cli.Known("snap-revision", nil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision")
+}
+
+func (cs *clientSuite) TestClientAssertsCallsEndpointWithFilter(c *C) {
+ _, _ = cs.cli.Known("snap-revision", map[string]string{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": "sha3-384...",
+ })
+ u, err := url.ParseRequestURI(cs.req.URL.String())
+ c.Assert(err, IsNil)
+ c.Check(u.Path, Equals, "/v2/assertions/snap-revision")
+ c.Check(u.Query(), DeepEquals, url.Values{
+ "snap-sha3-384": []string{"sha3-384..."},
+ "snap-id": []string{"snap-id-1"},
+ })
+}
+
+func (cs *clientSuite) TestClientAssertsHttpError(c *C) {
+ cs.err = errors.New("fail")
+ _, err := cs.cli.Known("snap-build", nil)
+ c.Assert(err, ErrorMatches, "failed to query assertions: cannot communicate with server: fail")
+}
+
+func (cs *clientSuite) TestClientAssertsJSONError(c *C) {
+ cs.status = http.StatusBadRequest
+ cs.header = http.Header{}
+ cs.header.Add("Content-type", "application/json")
+ cs.rsp = `{
+ "status-code": 400,
+ "type": "error",
+ "result": {
+ "message": "invalid"
+ }
+ }`
+ _, err := cs.cli.Known("snap-build", nil)
+ c.Assert(err, ErrorMatches, "invalid")
+}
+
+func (cs *clientSuite) TestClientAsserts(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "2")
+ cs.rsp = `type: snap-revision
+authority-id: store-id1
+snap-sha3-384: P1wNUk5O_5tO5spqOLlqUuAk7gkNYezIMHp5N9hMUg1a6YEjNeaCc4T0BaYz7IWs
+snap-id: snap-id-1
+snap-size: 123
+snap-revision: 1
+developer-id: dev-id1
+revision: 1
+timestamp: 2015-11-25T20:00:00Z
+body-length: 0
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+openpgp ...
+
+type: snap-revision
+authority-id: store-id1
+snap-sha3-384: 0Yt6-GXQeTZWUAHo1IKDpS9kqO6zMaizY6vGEfGM-aSfpghPKir1Ic7teQ5Zadaj
+snap-id: snap-id-2
+snap-size: 456
+snap-revision: 1
+developer-id: dev-id1
+revision: 1
+timestamp: 2015-11-30T20:00:00Z
+body-length: 0
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+openpgp ...
+`
+
+ a, err := cs.cli.Known("snap-revision", nil)
+ c.Assert(err, IsNil)
+ c.Check(a, HasLen, 2)
+
+ c.Check(a[0].Type(), Equals, asserts.SnapRevisionType)
+}
+
+func (cs *clientSuite) TestClientAssertsNoAssertions(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "0")
+ cs.rsp = ""
+ cs.status = http.StatusOK
+ a, err := cs.cli.Known("snap-revision", nil)
+ c.Assert(err, IsNil)
+ c.Check(a, HasLen, 0)
+}
+
+func (cs *clientSuite) TestClientAssertsMissingAssertions(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "4")
+ cs.rsp = ""
+ cs.status = http.StatusOK
+ _, err := cs.cli.Known("snap-build", nil)
+ c.Assert(err, ErrorMatches, "response did not have the expected number of assertions")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+
+ "github.com/snapcore/snapd/store"
+)
+
+func (client *Client) Buy(opts *store.BuyOptions) (*store.BuyResult, error) {
+ if opts == nil {
+ opts = &store.BuyOptions{}
+ }
+
+ var body bytes.Buffer
+ if err := json.NewEncoder(&body).Encode(opts); err != nil {
+ return nil, err
+ }
+
+ var result store.BuyResult
+ _, err := client.doSync("POST", "/v2/buy", nil, nil, &body, &result)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+func (client *Client) ReadyToBuy() error {
+ var result bool
+ _, err := client.doSync("GET", "/v2/buy/ready", nil, nil, nil, &result)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "time"
+)
+
+// A Change is a modification to the system state.
+type Change struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Status string `json:"status"`
+ Tasks []*Task `json:"tasks,omitempty"`
+ Ready bool `json:"ready"`
+ Err string `json:"err,omitempty"`
+
+ SpawnTime time.Time `json:"spawn-time,omitempty"`
+ ReadyTime time.Time `json:"ready-time,omitempty"`
+
+ data map[string]*json.RawMessage
+}
+
+var ErrNoData = fmt.Errorf("data entry not found")
+
+// Get unmarshals into value the kind-specific data with the provided key.
+func (c *Change) Get(key string, value interface{}) error {
+ raw := c.data[key]
+ if raw == nil {
+ return ErrNoData
+ }
+ return json.Unmarshal([]byte(*raw), value)
+}
+
+// A Task is an operation done to change the system's state.
+type Task struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Status string `json:"status"`
+ Log []string `json:"log,omitempty"`
+ Progress TaskProgress `json:"progress"`
+
+ SpawnTime time.Time `json:"spawn-time,omitempty"`
+ ReadyTime time.Time `json:"ready-time,omitempty"`
+}
+
+type TaskProgress struct {
+ Label string `json:"label"`
+ Done int `json:"done"`
+ Total int `json:"total"`
+}
+
+type changeAndData struct {
+ Change
+ Data map[string]*json.RawMessage `json:"data"`
+}
+
+// Change fetches information about a Change given its ID
+func (client *Client) Change(id string) (*Change, error) {
+ var chgd changeAndData
+ _, err := client.doSync("GET", "/v2/changes/"+id, nil, nil, nil, &chgd)
+ if err != nil {
+ return nil, err
+ }
+
+ chgd.Change.data = chgd.Data
+ return &chgd.Change, nil
+}
+
+// Abort attempts to abort a change that is in not yet ready.
+func (client *Client) Abort(id string) (*Change, error) {
+ var postData struct {
+ Action string `json:"action"`
+ }
+ postData.Action = "abort"
+
+ var body bytes.Buffer
+ if err := json.NewEncoder(&body).Encode(postData); err != nil {
+ return nil, err
+ }
+
+ var chg Change
+ if _, err := client.doSync("POST", "/v2/changes/"+id, nil, nil, &body, &chg); err != nil {
+ return nil, err
+ }
+
+ return &chg, nil
+}
+
+type ChangeSelector uint8
+
+func (c ChangeSelector) String() string {
+ switch c {
+ case ChangesInProgress:
+ return "in-progress"
+ case ChangesReady:
+ return "ready"
+ case ChangesAll:
+ return "all"
+ }
+
+ panic(fmt.Sprintf("unknown ChangeSelector %d", c))
+}
+
+const (
+ ChangesInProgress ChangeSelector = 1 << iota
+ ChangesReady
+ ChangesAll = ChangesReady | ChangesInProgress
+)
+
+type ChangesOptions struct {
+ SnapName string // if empty, no filtering by name is done
+ Selector ChangeSelector
+}
+
+func (client *Client) Changes(opts *ChangesOptions) ([]*Change, error) {
+ query := url.Values{}
+ if opts != nil {
+ if opts.Selector != 0 {
+ query.Set("select", opts.Selector.String())
+ }
+ if opts.SnapName != "" {
+ query.Set("for", opts.SnapName)
+ }
+ }
+
+ var chgds []changeAndData
+ _, err := client.doSync("GET", "/v2/changes", query, nil, nil, &chgds)
+ if err != nil {
+ return nil, err
+ }
+
+ var chgs []*Change
+ for i := range chgds {
+ chgd := &chgds[i]
+ chgd.Change.data = chgd.Data
+ chgs = append(chgs, &chgd.Change)
+ }
+
+ return chgs, err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "io/ioutil"
+ "time"
+)
+
+func (cs *clientSuite) TestClientChange(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": {
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Do",
+ "ready": false,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:04Z",
+ "tasks": [{"kind": "bar", "summary": "...", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}]
+}}`
+
+ chg, err := cs.cli.Change("uno")
+ c.Assert(err, check.IsNil)
+ c.Check(chg, check.DeepEquals, &client.Change{
+ ID: "uno",
+ Kind: "foo",
+ Summary: "...",
+ Status: "Do",
+ Tasks: []*client.Task{{
+ Kind: "bar",
+ Summary: "...",
+ Status: "Do",
+ Progress: client.TaskProgress{Done: 0, Total: 1},
+ SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC),
+ ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC),
+ }},
+
+ SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC),
+ ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC),
+ })
+}
+
+func (cs *clientSuite) TestClientChangeData(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": {
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Do",
+ "ready": false,
+ "data": {"n": 42}
+}}`
+
+ chg, err := cs.cli.Change("uno")
+ c.Assert(err, check.IsNil)
+ var n int
+ err = chg.Get("n", &n)
+ c.Assert(err, check.IsNil)
+ c.Assert(n, check.Equals, 42)
+
+ err = chg.Get("missing", &n)
+ c.Assert(err, check.Equals, client.ErrNoData)
+}
+
+func (cs *clientSuite) TestClientChangeError(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": {
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Error",
+ "ready": true,
+ "tasks": [{"kind": "bar", "summary": "...", "status": "Error", "progress": {"done": 1, "total": 1}, "log": ["ERROR: something broke"]}],
+ "err": "error message"
+}}`
+
+ chg, err := cs.cli.Change("uno")
+ c.Assert(err, check.IsNil)
+ c.Check(chg, check.DeepEquals, &client.Change{
+ ID: "uno",
+ Kind: "foo",
+ Summary: "...",
+ Status: "Error",
+ Tasks: []*client.Task{{
+ Kind: "bar",
+ Summary: "...",
+ Status: "Error",
+ Progress: client.TaskProgress{Done: 1, Total: 1},
+ Log: []string{"ERROR: something broke"},
+ }},
+ Err: "error message",
+ Ready: true,
+ })
+}
+
+func (cs *clientSuite) TestClientChangesString(c *check.C) {
+ for k, v := range map[client.ChangeSelector]string{
+ client.ChangesAll: "all",
+ client.ChangesReady: "ready",
+ client.ChangesInProgress: "in-progress",
+ } {
+ c.Check(k.String(), check.Equals, v)
+ }
+}
+
+func (cs *clientSuite) TestClientChanges(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": [{
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Do",
+ "ready": false,
+ "tasks": [{"kind": "bar", "summary": "...", "status": "Do", "progress": {"done": 0, "total": 1}}]
+}]}`
+
+ for _, i := range []*client.ChangesOptions{
+ {Selector: client.ChangesAll},
+ {Selector: client.ChangesReady},
+ {Selector: client.ChangesInProgress},
+ {SnapName: "foo"},
+ nil,
+ } {
+ chg, err := cs.cli.Changes(i)
+ c.Assert(err, check.IsNil)
+ c.Check(chg, check.DeepEquals, []*client.Change{{
+ ID: "uno",
+ Kind: "foo",
+ Summary: "...",
+ Status: "Do",
+ Tasks: []*client.Task{{Kind: "bar", Summary: "...", Status: "Do", Progress: client.TaskProgress{Done: 0, Total: 1}}},
+ }})
+ if i == nil {
+ c.Check(cs.req.URL.RawQuery, check.Equals, "")
+ } else {
+ if i.Selector != 0 {
+ c.Check(cs.req.URL.RawQuery, check.Equals, "select="+i.Selector.String())
+ } else {
+ c.Check(cs.req.URL.RawQuery, check.Equals, "for="+i.SnapName)
+ }
+ }
+ }
+
+}
+
+func (cs *clientSuite) TestClientChangesData(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": [{
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Do",
+ "ready": false,
+ "data": {"n": 42}
+}]}`
+
+ chgs, err := cs.cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll})
+ c.Assert(err, check.IsNil)
+
+ chg := chgs[0]
+ var n int
+ err = chg.Get("n", &n)
+ c.Assert(err, check.IsNil)
+ c.Assert(n, check.Equals, 42)
+
+ err = chg.Get("missing", &n)
+ c.Assert(err, check.Equals, client.ErrNoData)
+}
+
+func (cs *clientSuite) TestClientAbort(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": {
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Hold",
+ "ready": true,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:04Z"
+}}`
+
+ chg, err := cs.cli.Abort("uno")
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(chg, check.DeepEquals, &client.Change{
+ ID: "uno",
+ Kind: "foo",
+ Summary: "...",
+ Status: "Hold",
+ Ready: true,
+
+ SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC),
+ ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC),
+ })
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(string(body), check.Equals, "{\"action\":\"abort\"}\n")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "syscall"
+ "time"
+
+ "github.com/snapcore/snapd/dirs"
+)
+
+func unixDialer() func(string, string) (net.Conn, error) {
+ // We have two sockets available: the SnapdSocket (which provides
+ // administrative access), and the SnapSocket (which doesn't). Use the most
+ // powerful one available (e.g. from within snaps, SnapdSocket is hidden by
+ // apparmor unless the snap has the snapd-control interface).
+ socketPath := dirs.SnapdSocket
+ file, err := os.OpenFile(socketPath, os.O_RDWR, 0666)
+ if err == nil {
+ file.Close()
+ } else if e, ok := err.(*os.PathError); ok && (e.Err == syscall.ENOENT || e.Err == syscall.EACCES) {
+ socketPath = dirs.SnapSocket
+ }
+
+ return func(_, _ string) (net.Conn, error) {
+ return net.Dial("unix", socketPath)
+ }
+}
+
+type doer interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Config allows to customize client behavior.
+type Config struct {
+ // BaseURL contains the base URL where snappy daemon is expected to be.
+ // It can be empty for a default behavior of talking over a unix socket.
+ BaseURL string
+}
+
+// A Client knows how to talk to the snappy daemon.
+type Client struct {
+ baseURL url.URL
+ doer doer
+}
+
+// New returns a new instance of Client
+func New(config *Config) *Client {
+ // By default talk over an UNIX socket.
+ if config == nil || config.BaseURL == "" {
+ return &Client{
+ baseURL: url.URL{
+ Scheme: "http",
+ Host: "localhost",
+ },
+ doer: &http.Client{
+ Transport: &http.Transport{Dial: unixDialer()},
+ },
+ }
+ }
+ baseURL, err := url.Parse(config.BaseURL)
+ if err != nil {
+ panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err))
+ }
+ return &Client{
+ baseURL: *baseURL,
+ doer: &http.Client{},
+ }
+}
+
+func (client *Client) setAuthorization(req *http.Request) error {
+ user, err := readAuthData()
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon)
+ for _, discharge := range user.Discharges {
+ fmt.Fprintf(&buf, `, discharge="%s"`, discharge)
+ }
+ req.Header.Set("Authorization", buf.String())
+ return nil
+}
+
+type RequestError struct{ error }
+
+func (e RequestError) Error() string {
+ return fmt.Sprintf("cannot build request: %v", e.error)
+}
+
+type AuthorizationError struct{ error }
+
+func (e AuthorizationError) Error() string {
+ return fmt.Sprintf("cannot add authorization: %v", e.error)
+}
+
+type ConnectionError struct{ error }
+
+func (e ConnectionError) Error() string {
+ return fmt.Sprintf("cannot communicate with server: %v", e.error)
+}
+
+// raw performs a request and returns the resulting http.Response and
+// error you usually only need to call this directly if you expect the
+// response to not be JSON, otherwise you'd call Do(...) instead.
+func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
+ // fake a url to keep http.Client happy
+ u := client.baseURL
+ u.Path = path.Join(client.baseURL.Path, urlpath)
+ u.RawQuery = query.Encode()
+ req, err := http.NewRequest(method, u.String(), body)
+ if err != nil {
+ return nil, RequestError{err}
+ }
+
+ for key, value := range headers {
+ req.Header.Set(key, value)
+ }
+
+ // set Authorization header if there are user's credentials
+ err = client.setAuthorization(req)
+ if err != nil {
+ return nil, AuthorizationError{err}
+ }
+
+ rsp, err := client.doer.Do(req)
+ if err != nil {
+ return nil, ConnectionError{err}
+ }
+
+ return rsp, nil
+}
+
+var (
+ doRetry = 250 * time.Millisecond
+ doTimeout = 5 * time.Second
+)
+
+// MockDoRetry mocks the delays used by the do retry loop.
+func MockDoRetry(retry, timeout time.Duration) (restore func()) {
+ oldRetry := doRetry
+ oldTimeout := doTimeout
+ doRetry = retry
+ doTimeout = timeout
+ return func() {
+ doRetry = oldRetry
+ doTimeout = oldTimeout
+ }
+}
+
+// do performs a request and decodes the resulting json into the given
+// value. It's low-level, for testing/experimenting only; you should
+// usually use a higher level interface that builds on this.
+func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) error {
+ retry := time.NewTicker(doRetry)
+ defer retry.Stop()
+ timeout := time.After(doTimeout)
+ var rsp *http.Response
+ var err error
+ for {
+ rsp, err = client.raw(method, path, query, headers, body)
+ if err == nil || method != "GET" {
+ break
+ }
+ select {
+ case <-retry.C:
+ continue
+ case <-timeout:
+ }
+ break
+ }
+ if err != nil {
+ return err
+ }
+ defer rsp.Body.Close()
+
+ if v != nil {
+ dec := json.NewDecoder(rsp.Body)
+ if err := dec.Decode(v); err != nil {
+ r := dec.Buffered()
+ buf, err1 := ioutil.ReadAll(r)
+ if err1 != nil {
+ buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1))
+ }
+ return fmt.Errorf("cannot decode %q: %s", buf, err)
+ }
+ }
+
+ return nil
+}
+
+// doSync performs a request to the given path using the specified HTTP method.
+// It expects a "sync" response from the API and on success decodes the JSON
+// response payload into the given value.
+func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) {
+ var rsp response
+ if err := client.do(method, path, query, headers, body, &rsp); err != nil {
+ return nil, err
+ }
+ if err := rsp.err(); err != nil {
+ return nil, err
+ }
+ if rsp.Type != "sync" {
+ return nil, fmt.Errorf("expected sync response, got %q", rsp.Type)
+ }
+
+ if v != nil {
+ if err := json.Unmarshal(rsp.Result, v); err != nil {
+ return nil, fmt.Errorf("cannot unmarshal: %v", err)
+ }
+ }
+
+ return &rsp.ResultInfo, nil
+}
+
+func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) {
+ var rsp response
+
+ if err := client.do(method, path, query, headers, body, &rsp); err != nil {
+ return "", err
+ }
+ if err := rsp.err(); err != nil {
+ return "", err
+ }
+ if rsp.Type != "async" {
+ return "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type)
+ }
+ if rsp.StatusCode != http.StatusAccepted {
+ return "", fmt.Errorf("operation not accepted")
+ }
+ if rsp.Change == "" {
+ return "", fmt.Errorf("async response without change reference")
+ }
+
+ return rsp.Change, nil
+}
+
+type ServerVersion struct {
+ Version string
+ Series string
+ OSID string
+ OSVersionID string
+ OnClassic bool
+}
+
+func (client *Client) ServerVersion() (*ServerVersion, error) {
+ sysInfo, err := client.SysInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ return &ServerVersion{
+ Version: sysInfo.Version,
+ Series: sysInfo.Series,
+ OSID: sysInfo.OSRelease.ID,
+ OSVersionID: sysInfo.OSRelease.VersionID,
+ OnClassic: sysInfo.OnClassic,
+ }, nil
+}
+
+// A response produced by the REST API will usually fit in this
+// (exceptions are the icons/ endpoints obvs)
+type response struct {
+ Result json.RawMessage `json:"result"`
+ Status string `json:"status"`
+ StatusCode int `json:"status-code"`
+ Type string `json:"type"`
+ Change string `json:"change"`
+
+ ResultInfo
+}
+
+// Error is the real value of response.Result when an error occurs.
+type Error struct {
+ Kind string `json:"kind"`
+ Message string `json:"message"`
+
+ StatusCode int
+}
+
+func (e *Error) Error() string {
+ return e.Message
+}
+
+const (
+ ErrorKindTwoFactorRequired = "two-factor-required"
+ ErrorKindTwoFactorFailed = "two-factor-failed"
+ ErrorKindLoginRequired = "login-required"
+ ErrorKindTermsNotAccepted = "terms-not-accepted"
+ ErrorKindNoPaymentMethods = "no-payment-methods"
+ ErrorKindPaymentDeclined = "payment-declined"
+
+ ErrorKindSnapAlreadyInstalled = "snap-already-installed"
+ ErrorKindSnapNotInstalled = "snap-not-installed"
+ ErrorKindNoUpdateAvailable = "snap-no-update-available"
+)
+
+// IsTwoFactorError returns whether the given error is due to problems
+// in two-factor authentication.
+func IsTwoFactorError(err error) bool {
+ e, ok := err.(*Error)
+ if !ok || e == nil {
+ return false
+ }
+
+ return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired
+}
+
+// OSRelease contains information about the system extracted from /etc/os-release.
+type OSRelease struct {
+ ID string `json:"id"`
+ VersionID string `json:"version-id,omitempty"`
+}
+
+// SysInfo holds system information
+type SysInfo struct {
+ Series string `json:"series,omitempty"`
+ Version string `json:"version,omitempty"`
+ OSRelease OSRelease `json:"os-release"`
+ OnClassic bool `json:"on-classic"`
+ Managed bool `json:"managed"`
+}
+
+func (rsp *response) err() error {
+ if rsp.Type != "error" {
+ return nil
+ }
+ var resultErr Error
+ err := json.Unmarshal(rsp.Result, &resultErr)
+ if err != nil || resultErr.Message == "" {
+ return fmt.Errorf("server error: %q", rsp.Status)
+ }
+ resultErr.StatusCode = rsp.StatusCode
+
+ return &resultErr
+}
+
+func parseError(r *http.Response) error {
+ var rsp response
+ if r.Header.Get("Content-Type") != "application/json" {
+ return fmt.Errorf("server error: %q", r.Status)
+ }
+
+ dec := json.NewDecoder(r.Body)
+ if err := dec.Decode(&rsp); err != nil {
+ return fmt.Errorf("cannot unmarshal error: %v", err)
+ }
+
+ err := rsp.err()
+ if err == nil {
+ return fmt.Errorf("server error: %q", r.Status)
+ }
+ return err
+}
+
+// SysInfo gets system information from the REST API.
+func (client *Client) SysInfo() (*SysInfo, error) {
+ var sysInfo SysInfo
+
+ if _, err := client.doSync("GET", "/v2/system-info", nil, nil, nil, &sysInfo); err != nil {
+ return nil, fmt.Errorf("cannot obtain system details: %v", err)
+ }
+
+ return &sysInfo, nil
+}
+
+// CreateUserResult holds the result of a user creation.
+type CreateUserResult struct {
+ Username string `json:"username"`
+ SSHKeys []string `json:"ssh-keys"`
+}
+
+// CreateUserOptions holds options for creating a local system user.
+//
+// If Known is false, the provided email is used to query the store for
+// username and SSH key details.
+//
+// If Known is true, the user will be created by looking through existing
+// system-user assertions and looking for a matching email. If Email is
+// empty then all such assertions are considered and multiple users may
+// be created.
+type CreateUserOptions struct {
+ Email string `json:"email,omitempty"`
+ Sudoer bool `json:"sudoer,omitempty"`
+ Known bool `json:"known,omitempty"`
+ ForceManaged bool `json:"force-managed,omitempty"`
+}
+
+// CreateUser creates a local system user. See CreateUserOptions for details.
+func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) {
+ if options.Email == "" {
+ return nil, fmt.Errorf("cannot create a user without providing an email")
+ }
+
+ var result CreateUserResult
+ data, err := json.Marshal(options)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
+ return nil, fmt.Errorf("while creating user: %v", err)
+ }
+ return &result, nil
+}
+
+// CreateUsers creates multiple local system users. See CreateUserOptions for details.
+//
+// Results may be provided even if there are errors.
+func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) {
+ for _, opts := range options {
+ if opts.Email == "" && !opts.Known {
+ return nil, fmt.Errorf("cannot create user from store details without an email to query for")
+ }
+ }
+
+ var results []*CreateUserResult
+ var errs []error
+
+ for _, opts := range options {
+ data, err := json.Marshal(opts)
+ if err != nil {
+ return nil, err
+ }
+
+ if opts.Email == "" {
+ var result []*CreateUserResult
+ if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
+ errs = append(errs, err)
+ } else {
+ results = append(results, result...)
+ }
+ } else {
+ var result *CreateUserResult
+ if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
+ errs = append(errs, err)
+ } else {
+ results = append(results, result)
+ }
+ }
+ }
+
+ if len(errs) == 1 {
+ return results, errs[0]
+ }
+ if len(errs) > 1 {
+ var buf bytes.Buffer
+ for _, err := range errs {
+ fmt.Fprintf(&buf, "\n- %s", err)
+ }
+ return results, fmt.Errorf("while creating users:%s", buf.Bytes())
+ }
+ return results, nil
+}
+
+// Users returns the local users.
+func (client *Client) Users() ([]*User, error) {
+ var result []*User
+
+ if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil {
+ return nil, fmt.Errorf("while getting users: %v", err)
+ }
+ return result, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type clientSuite struct {
+ cli *client.Client
+ req *http.Request
+ reqs []*http.Request
+ rsp string
+ rsps []string
+ err error
+ doCalls int
+ header http.Header
+ status int
+}
+
+var _ = Suite(&clientSuite{})
+
+func (cs *clientSuite) SetUpTest(c *C) {
+ os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "auth.json"))
+ cs.cli = client.New(nil)
+ cs.cli.SetDoer(cs)
+ cs.err = nil
+ cs.req = nil
+ cs.reqs = nil
+ cs.rsp = ""
+ cs.rsps = nil
+ cs.req = nil
+ cs.header = nil
+ cs.status = http.StatusOK
+ cs.doCalls = 0
+
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (cs *clientSuite) TearDownTest(c *C) {
+ os.Unsetenv(client.TestAuthFileEnvKey)
+}
+
+func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) {
+ cs.req = req
+ cs.reqs = append(cs.reqs, req)
+ body := cs.rsp
+ if cs.doCalls < len(cs.rsps) {
+ body = cs.rsps[cs.doCalls]
+ }
+ rsp := &http.Response{
+ Body: ioutil.NopCloser(strings.NewReader(body)),
+ Header: cs.header,
+ StatusCode: cs.status,
+ }
+ cs.doCalls++
+ return rsp, cs.err
+}
+
+func (cs *clientSuite) TestNewPanics(c *C) {
+ c.Assert(func() {
+ client.New(&client.Config{BaseURL: ":"})
+ }, PanicMatches, `cannot parse server base URL: ":" \(parse :: missing protocol scheme\)`)
+}
+
+func (cs *clientSuite) TestClientDoReportsErrors(c *C) {
+ restore := client.MockDoRetry(10*time.Millisecond, 100*time.Millisecond)
+ defer restore()
+ cs.err = errors.New("ouchie")
+ err := cs.cli.Do("GET", "/", nil, nil, nil)
+ c.Check(err, ErrorMatches, "cannot communicate with server: ouchie")
+ if cs.doCalls < 2 {
+ c.Fatalf("do did not retry")
+ }
+}
+
+func (cs *clientSuite) TestClientWorks(c *C) {
+ var v []int
+ cs.rsp = `[1,2]`
+ reqBody := ioutil.NopCloser(strings.NewReader(""))
+ err := cs.cli.Do("GET", "/this", nil, reqBody, &v)
+ c.Check(err, IsNil)
+ c.Check(v, DeepEquals, []int{1, 2})
+ c.Assert(cs.req, NotNil)
+ c.Assert(cs.req.URL, NotNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.Body, Equals, reqBody)
+ c.Check(cs.req.URL.Path, Equals, "/this")
+}
+
+func (cs *clientSuite) TestClientDefaultsToNoAuthorization(c *C) {
+ os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json"))
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ var v string
+ _ = cs.cli.Do("GET", "/this", nil, nil, &v)
+ c.Assert(cs.req, NotNil)
+ authorization := cs.req.Header.Get("Authorization")
+ c.Check(authorization, Equals, "")
+}
+
+func (cs *clientSuite) TestClientSetsAuthorization(c *C) {
+ os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json"))
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ mockUserData := client.User{
+ Macaroon: "macaroon",
+ Discharges: []string{"discharge"},
+ }
+ err := client.TestWriteAuth(mockUserData)
+ c.Assert(err, IsNil)
+
+ var v string
+ _ = cs.cli.Do("GET", "/this", nil, nil, &v)
+ authorization := cs.req.Header.Get("Authorization")
+ c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`)
+}
+
+func (cs *clientSuite) TestClientSysInfo(c *C) {
+ cs.rsp = `{"type": "sync", "result":
+ {"series": "16",
+ "version": "2",
+ "os-release": {"id": "ubuntu", "version-id": "16.04"},
+ "on-classic": true}}`
+ sysInfo, err := cs.cli.SysInfo()
+ c.Check(err, IsNil)
+ c.Check(sysInfo, DeepEquals, &client.SysInfo{
+ Version: "2",
+ Series: "16",
+ OSRelease: client.OSRelease{
+ ID: "ubuntu",
+ VersionID: "16.04",
+ },
+ OnClassic: true,
+ })
+}
+
+func (cs *clientSuite) TestServerVersion(c *C) {
+ cs.rsp = `{"type": "sync", "result":
+ {"series": "16",
+ "version": "2",
+ "os-release": {"id": "zyggy", "version-id": "123"}}}`
+ version, err := cs.cli.ServerVersion()
+ c.Check(err, IsNil)
+ c.Check(version, DeepEquals, &client.ServerVersion{
+ Version: "2",
+ Series: "16",
+ OSID: "zyggy",
+ OSVersionID: "123",
+ })
+}
+
+func (cs *clientSuite) TestSnapdClientIntegration(c *C) {
+ c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdSocket), 0755), IsNil)
+ l, err := net.Listen("unix", dirs.SnapdSocket)
+ if err != nil {
+ c.Fatalf("unable to listen on %q: %v", dirs.SnapdSocket, err)
+ }
+
+ f := func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, Equals, "/v2/system-info")
+ c.Check(r.URL.RawQuery, Equals, "")
+
+ fmt.Fprintln(w, `{"type":"sync", "result":{"series":"42"}}`)
+ }
+
+ srv := &httptest.Server{
+ Listener: l,
+ Config: &http.Server{Handler: http.HandlerFunc(f)},
+ }
+ srv.Start()
+ defer srv.Close()
+
+ cli := client.New(nil)
+ si, err := cli.SysInfo()
+ c.Check(err, IsNil)
+ c.Check(si.Series, Equals, "42")
+}
+
+func (cs *clientSuite) TestSnapClientIntegration(c *C) {
+ c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapSocket), 0755), IsNil)
+ l, err := net.Listen("unix", dirs.SnapSocket)
+ if err != nil {
+ c.Fatalf("unable to listen on %q: %v", dirs.SnapSocket, err)
+ }
+
+ f := func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, Equals, "/v2/snapctl")
+ c.Check(r.URL.RawQuery, Equals, "")
+
+ fmt.Fprintln(w, `{"type":"sync", "result":{"stdout":"test stdout","stderr":"test stderr"}}`)
+ }
+
+ srv := &httptest.Server{
+ Listener: l,
+ Config: &http.Server{Handler: http.HandlerFunc(f)},
+ }
+ srv.Start()
+ defer srv.Close()
+
+ cli := client.New(nil)
+ options := &client.SnapCtlOptions{
+ ContextID: "foo",
+ Args: []string{"bar", "--baz"},
+ }
+
+ stdout, stderr, err := cli.RunSnapctl(options)
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "test stdout")
+ c.Check(string(stderr), Equals, "test stderr")
+}
+
+func (cs *clientSuite) TestClientReportsOpError(c *C) {
+ cs.rsp = `{"type": "error", "status": "potatoes"}`
+ _, err := cs.cli.SysInfo()
+ c.Check(err, ErrorMatches, `.*server error: "potatoes"`)
+}
+
+func (cs *clientSuite) TestClientReportsOpErrorStr(c *C) {
+ cs.rsp = `{
+ "result": {},
+ "status": "Bad Request",
+ "status-code": 400,
+ "type": "error"
+ }`
+ _, err := cs.cli.SysInfo()
+ c.Check(err, ErrorMatches, `.*server error: "Bad Request"`)
+}
+
+func (cs *clientSuite) TestClientReportsBadType(c *C) {
+ cs.rsp = `{"type": "what"}`
+ _, err := cs.cli.SysInfo()
+ c.Check(err, ErrorMatches, `.*expected sync response, got "what"`)
+}
+
+func (cs *clientSuite) TestClientReportsOuterJSONError(c *C) {
+ cs.rsp = "this isn't really json is it"
+ _, err := cs.cli.SysInfo()
+ c.Check(err, ErrorMatches, `.*invalid character .*`)
+}
+
+func (cs *clientSuite) TestClientReportsInnerJSONError(c *C) {
+ cs.rsp = `{"type": "sync", "result": "this isn't really json is it"}`
+ _, err := cs.cli.SysInfo()
+ c.Check(err, ErrorMatches, `.*cannot unmarshal.*`)
+}
+
+func (cs *clientSuite) TestParseError(c *C) {
+ resp := &http.Response{
+ Status: "404 Not Found",
+ }
+ err := client.ParseErrorInTest(resp)
+ c.Check(err, ErrorMatches, `server error: "404 Not Found"`)
+
+ h := http.Header{}
+ h.Add("Content-Type", "application/json")
+ resp = &http.Response{
+ Status: "400 Bad Request",
+ Header: h,
+ Body: ioutil.NopCloser(strings.NewReader(`{
+ "status-code": 400,
+ "type": "error",
+ "result": {
+ "message": "invalid"
+ }
+ }`)),
+ }
+ err = client.ParseErrorInTest(resp)
+ c.Check(err, ErrorMatches, "invalid")
+
+ resp = &http.Response{
+ Status: "400 Bad Request",
+ Header: h,
+ Body: ioutil.NopCloser(strings.NewReader("{}")),
+ }
+ err = client.ParseErrorInTest(resp)
+ c.Check(err, ErrorMatches, `server error: "400 Bad Request"`)
+}
+
+func (cs *clientSuite) TestIsTwoFactor(c *C) {
+ c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorRequired}), Equals, true)
+ c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorFailed}), Equals, true)
+ c.Check(client.IsTwoFactorError(&client.Error{Kind: "some other kind"}), Equals, false)
+ c.Check(client.IsTwoFactorError(errors.New("test")), Equals, false)
+ c.Check(client.IsTwoFactorError(nil), Equals, false)
+ c.Check(client.IsTwoFactorError((*client.Error)(nil)), Equals, false)
+}
+
+func (cs *clientSuite) TestClientCreateUser(c *C) {
+ _, err := cs.cli.CreateUser(&client.CreateUserOptions{})
+ c.Assert(err, ErrorMatches, "cannot create a user without providing an email")
+
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "username": "karl",
+ "ssh-keys": ["one", "two"]
+ }
+ }`
+ rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true})
+ c.Assert(cs.req.Method, Equals, "POST")
+ c.Assert(cs.req.URL.Path, Equals, "/v2/create-user")
+ c.Assert(err, IsNil)
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, `{"email":"one@email.com","sudoer":true,"known":true}`)
+
+ c.Assert(rsp, DeepEquals, &client.CreateUserResult{
+ Username: "karl",
+ SSHKeys: []string{"one", "two"},
+ })
+}
+
+var createUsersTests = []struct {
+ options []*client.CreateUserOptions
+ bodies []string
+ responses []string
+ results []*client.CreateUserResult
+ error string
+}{{
+ options: []*client.CreateUserOptions{{}},
+ error: "cannot create user from store details without an email to query for",
+}, {
+ options: []*client.CreateUserOptions{{
+ Email: "one@example.com",
+ Sudoer: true,
+ }, {
+ Known: true,
+ }},
+ bodies: []string{
+ `{"email":"one@example.com","sudoer":true}`,
+ `{"known":true}`,
+ },
+ responses: []string{
+ `{"type": "sync", "result": {"username": "one", "ssh-keys":["a", "b"]}}`,
+ `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`,
+ },
+ results: []*client.CreateUserResult{{
+ Username: "one",
+ SSHKeys: []string{"a", "b"},
+ }, {
+ Username: "two",
+ }, {
+ Username: "three",
+ }},
+}}
+
+func (cs *clientSuite) TestClientCreateUsers(c *C) {
+ for _, test := range createUsersTests {
+ cs.rsps = test.responses
+
+ results, err := cs.cli.CreateUsers(test.options)
+ if test.error != "" {
+ c.Assert(err, ErrorMatches, test.error)
+ }
+ c.Assert(results, DeepEquals, test.results)
+
+ var bodies []string
+ for _, req := range cs.reqs {
+ c.Assert(req.Method, Equals, "POST")
+ c.Assert(req.URL.Path, Equals, "/v2/create-user")
+ data, err := ioutil.ReadAll(req.Body)
+ c.Assert(err, IsNil)
+ bodies = append(bodies, string(data))
+ }
+
+ c.Assert(bodies, DeepEquals, test.bodies)
+ }
+}
+
+func (cs *clientSuite) TestClientJSONError(c *C) {
+ cs.rsp = `some non-json error message`
+ _, err := cs.cli.SysInfo()
+ c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`)
+}
+
+func (cs *clientSuite) TestUsers(c *C) {
+ cs.rsp = `{"type": "sync", "result":
+ [{"username": "foo","email":"foo@example.com"},
+ {"username": "bar","email":"bar@example.com"}]}`
+ sysInfo, err := cs.cli.Users()
+ c.Check(err, IsNil)
+ c.Check(sysInfo, DeepEquals, []*client.User{
+ {Username: "foo", Email: "foo@example.com"},
+ {Username: "bar", Email: "bar@example.com"},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/url"
+ "strings"
+)
+
+// SetConf requests a snap to apply the provided patch to the configuration.
+func (client *Client) SetConf(snapName string, patch map[string]interface{}) (changeID string, err error) {
+ b, err := json.Marshal(patch)
+ if err != nil {
+ return "", err
+ }
+ return client.doAsync("PUT", "/v2/snaps/"+snapName+"/conf", nil, nil, bytes.NewReader(b))
+}
+
+// Conf asks for a snap's current configuration.
+func (client *Client) Conf(snapName string, keys []string) (configuration map[string]interface{}, err error) {
+ // Prepare query
+ query := url.Values{}
+ query.Set("keys", strings.Join(keys, ","))
+
+ _, err = client.doSync("GET", "/v2/snaps/"+snapName+"/conf", query, nil, nil, &configuration)
+ if err != nil {
+ return nil, err
+ }
+
+ return configuration, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+
+ "gopkg.in/check.v1"
+)
+
+func (cs *clientSuite) TestClientSetConfCallsEndpoint(c *check.C) {
+ cs.cli.SetConf("snap-name", map[string]interface{}{"key": "value"})
+ c.Check(cs.req.Method, check.Equals, "PUT")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf")
+}
+
+func (cs *clientSuite) TestClientGetConfCallsEndpoint(c *check.C) {
+ cs.cli.Conf("snap-name", []string{"test-key"})
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf")
+ c.Check(cs.req.URL.Query().Get("keys"), check.Equals, "test-key")
+}
+
+func (cs *clientSuite) TestClientGetConfCallsEndpointMultipleKeys(c *check.C) {
+ cs.cli.Conf("snap-name", []string{"test-key1", "test-key2"})
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf")
+ c.Check(cs.req.URL.Query().Get("keys"), check.Equals, "test-key1,test-key2")
+}
+
+func (cs *clientSuite) TestClientSetConf(c *check.C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "foo"
+ }`
+ id, err := cs.cli.SetConf("snap-name", map[string]interface{}{"key": "value"})
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "foo")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "key": "value",
+ })
+}
+
+func (cs *clientSuite) TestClientGetConf(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {"test-key": "test-value"}
+ }`
+ value, err := cs.cli.Conf("snap-name", []string{"test-key"})
+ c.Assert(err, check.IsNil)
+ c.Check(value, check.DeepEquals, map[string]interface{}{"test-key": "test-value"})
+}
+
+func (cs *clientSuite) TestClientGetConfMultipleKeys(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {
+ "test-key1": "test-value1",
+ "test-key2": "test-value2"
+ }
+ }`
+ value, err := cs.cli.Conf("snap-name", []string{"test-key1", "test-key2"})
+ c.Assert(err, check.IsNil)
+ c.Check(value, check.DeepEquals, map[string]interface{}{
+ "test-key1": "test-value1",
+ "test-key2": "test-value2",
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "io"
+ "net/url"
+)
+
+// SetDoer sets the client's doer to the given one
+func (client *Client) SetDoer(d doer) {
+ client.doer = d
+}
+
+// Do does do.
+func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) error {
+ return client.do(method, path, query, nil, body, v)
+}
+
+// expose parseError for testing
+var ParseErrorInTest = parseError
+
+// expose read and write auth helpers for testing
+var TestWriteAuth = writeAuthData
+var TestReadAuth = readAuthData
+var TestStoreAuthFilename = storeAuthDataFilename
+
+var TestAuthFileEnvKey = authFileEnvKey
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+)
+
+// Icon represents the icon of an installed snap
+type Icon struct {
+ Filename string
+ Content []byte
+}
+
+// Icon returns the Icon belonging to an installed snap
+func (c *Client) Icon(pkgID string) (*Icon, error) {
+ const errPrefix = "cannot retrieve icon"
+
+ response, err := c.raw("GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil)
+ if err != nil {
+ return nil, fmt.Errorf("%s: failed to communicate with server: %s", errPrefix, err)
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("%s: Not Found", errPrefix)
+ }
+
+ re := regexp.MustCompile(`attachment; filename=(.+)`)
+ matches := re.FindStringSubmatch(response.Header.Get("Content-Disposition"))
+
+ if matches == nil || matches[1] == "" {
+ return nil, fmt.Errorf("%s: cannot determine filename", errPrefix)
+ }
+
+ content, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %s", errPrefix, err)
+ }
+
+ icon := &Icon{
+ Filename: matches[1],
+ Content: content,
+ }
+
+ return icon, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+)
+
+const (
+ pkgID = "chatroom.ogra"
+)
+
+func (cs *clientSuite) TestClientIconCallsEndpoint(c *C) {
+ _, _ = cs.cli.Icon(pkgID)
+ c.Assert(cs.req.Method, Equals, "GET")
+ c.Assert(cs.req.URL.Path, Equals, fmt.Sprintf("/v2/icons/%s/icon", pkgID))
+}
+
+func (cs *clientSuite) TestClientIconHttpError(c *C) {
+ cs.err = errors.New("fail")
+ _, err := cs.cli.Icon(pkgID)
+ c.Assert(err, ErrorMatches, ".*server: fail")
+}
+
+func (cs *clientSuite) TestClientIconResponseNotFound(c *C) {
+ cs.status = http.StatusNotFound
+ _, err := cs.cli.Icon(pkgID)
+ c.Assert(err, ErrorMatches, `.*Not Found`)
+}
+
+func (cs *clientSuite) TestClientIconInvalidContentDisposition(c *C) {
+ cs.header = http.Header{"Content-Disposition": {"invalid"}}
+ _, err := cs.cli.Icon(pkgID)
+ c.Assert(err, ErrorMatches, `.*cannot determine filename`)
+}
+
+func (cs *clientSuite) TestClientIcon(c *C) {
+ cs.rsp = "pixels"
+ cs.header = http.Header{"Content-Disposition": {"attachment; filename=myicon.png"}}
+ icon, err := cs.cli.Icon(pkgID)
+ c.Assert(err, IsNil)
+ c.Assert(icon.Filename, Equals, "myicon.png")
+ c.Assert(icon.Content, DeepEquals, []byte("pixels"))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+// Plug represents the potential of a given snap to connect to a slot.
+type Plug struct {
+ Snap string `json:"snap"`
+ Name string `json:"plug"`
+ Interface string `json:"interface,omitempty"`
+ Attrs map[string]interface{} `json:"attrs,omitempty"`
+ Apps []string `json:"apps,omitempty"`
+ Label string `json:"label,omitempty"`
+ Connections []SlotRef `json:"connections,omitempty"`
+}
+
+// PlugRef is a reference to a plug.
+type PlugRef struct {
+ Snap string `json:"snap"`
+ Name string `json:"plug"`
+}
+
+// Slot represents a capacity offered by a snap.
+type Slot struct {
+ Snap string `json:"snap"`
+ Name string `json:"slot"`
+ Interface string `json:"interface,omitempty"`
+ Attrs map[string]interface{} `json:"attrs,omitempty"`
+ Apps []string `json:"apps,omitempty"`
+ Label string `json:"label,omitempty"`
+ Connections []PlugRef `json:"connections,omitempty"`
+}
+
+// SlotRef is a reference to a slot.
+type SlotRef struct {
+ Snap string `json:"snap"`
+ Name string `json:"slot"`
+}
+
+// Interfaces contains information about all plugs, slots and their connections
+type Interfaces struct {
+ Plugs []Plug `json:"plugs"`
+ Slots []Slot `json:"slots"`
+}
+
+// InterfaceAction represents an action performed on the interface system.
+type InterfaceAction struct {
+ Action string `json:"action"`
+ Plugs []Plug `json:"plugs,omitempty"`
+ Slots []Slot `json:"slots,omitempty"`
+}
+
+// Interfaces returns all plugs, slots and their connections.
+func (client *Client) Interfaces() (interfaces Interfaces, err error) {
+ _, err = client.doSync("GET", "/v2/interfaces", nil, nil, nil, &interfaces)
+ return
+}
+
+// performInterfaceAction performs a single action on the interface system.
+func (client *Client) performInterfaceAction(sa *InterfaceAction) (changeID string, err error) {
+ b, err := json.Marshal(sa)
+ if err != nil {
+ return "", err
+ }
+ return client.doAsync("POST", "/v2/interfaces", nil, nil, bytes.NewReader(b))
+}
+
+// Connect establishes a connection between a plug and a slot.
+// The plug and the slot must have the same interface.
+func (client *Client) Connect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) {
+ return client.performInterfaceAction(&InterfaceAction{
+ Action: "connect",
+ Plugs: []Plug{{Snap: plugSnapName, Name: plugName}},
+ Slots: []Slot{{Snap: slotSnapName, Name: slotName}},
+ })
+}
+
+// Disconnect breaks the connection between a plug and a slot.
+func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) {
+ return client.performInterfaceAction(&InterfaceAction{
+ Action: "disconnect",
+ Plugs: []Plug{{Snap: plugSnapName, Name: plugName}},
+ Slots: []Slot{{Snap: slotSnapName, Name: slotName}},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+func (cs *clientSuite) TestClientInterfacesCallsEndpoint(c *check.C) {
+ _, _ = cs.cli.Interfaces()
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces")
+}
+
+func (cs *clientSuite) TestClientInterfaces(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "plugs": [
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-13",
+ "interface": "bool-file",
+ "label": "Pin 13",
+ "connections": [
+ {"snap": "keyboard-lights", "slot": "capslock-led"}
+ ]
+ }
+ ],
+ "slots": [
+ {
+ "snap": "keyboard-lights",
+ "slot": "capslock-led",
+ "interface": "bool-file",
+ "label": "Capslock indicator LED",
+ "connections": [
+ {"snap": "canonical-pi2", "plug": "pin-13"}
+ ]
+ }
+ ]
+ }
+ }`
+ interfaces, err := cs.cli.Interfaces()
+ c.Assert(err, check.IsNil)
+ c.Check(interfaces, check.DeepEquals, client.Interfaces{
+ Plugs: []client.Plug{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.SlotRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "bool-file",
+ Label: "Capslock indicator LED",
+ Connections: []client.PlugRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ },
+ },
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientConnectCallsEndpoint(c *check.C) {
+ cs.cli.Connect("producer", "plug", "consumer", "slot")
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces")
+}
+
+func (cs *clientSuite) TestClientConnect(c *check.C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "foo"
+ }`
+ id, err := cs.cli.Connect("producer", "plug", "consumer", "slot")
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "foo")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "action": "connect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientDisconnectCallsEndpoint(c *check.C) {
+ cs.cli.Disconnect("producer", "plug", "consumer", "slot")
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces")
+}
+
+func (cs *clientSuite) TestClientDisconnect(c *check.C) {
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "42"
+ }`
+ id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot")
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "42")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "action": "disconnect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+// User holds logged in user information.
+type User struct {
+ ID int `json:"id,omitempty"`
+ Username string `json:"username,omitempty"`
+ Email string `json:"email,omitempty"`
+
+ Macaroon string `json:"macaroon,omitempty"`
+ Discharges []string `json:"discharges,omitempty"`
+}
+
+type loginData struct {
+ Email string `json:"email,omitempty"`
+ Password string `json:"password,omitempty"`
+ Otp string `json:"otp,omitempty"`
+}
+
+// Login logs user in.
+func (client *Client) Login(email, password, otp string) (*User, error) {
+ postData := loginData{
+ Email: email,
+ Password: password,
+ Otp: otp,
+ }
+ var body bytes.Buffer
+ if err := json.NewEncoder(&body).Encode(postData); err != nil {
+ return nil, err
+ }
+
+ var user User
+ if _, err := client.doSync("POST", "/v2/login", nil, nil, &body, &user); err != nil {
+ return nil, err
+ }
+
+ if err := writeAuthData(user); err != nil {
+ return nil, fmt.Errorf("cannot persist login information: %v", err)
+ }
+ return &user, nil
+}
+
+// Logout logs the user out.
+func (client *Client) Logout() error {
+ _, err := client.doSync("POST", "/v2/logout", nil, nil, nil, nil)
+ if err != nil {
+ return err
+ }
+ return removeAuthData()
+}
+
+// LoggedInUser returns the logged in User or nil
+func (client *Client) LoggedInUser() *User {
+ u, err := readAuthData()
+ if err != nil {
+ return nil
+ }
+ return u
+}
+
+const authFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME"
+
+func storeAuthDataFilename(homeDir string) string {
+ if fn := os.Getenv(authFileEnvKey); fn != "" {
+ return fn
+ }
+
+ if homeDir == "" {
+ real, err := osutil.RealUser()
+ if err != nil {
+ panic(err)
+ }
+ homeDir = real.HomeDir
+ }
+
+ return filepath.Join(homeDir, ".snap", "auth.json")
+}
+
+// writeAuthData saves authentication details for later reuse through ReadAuthData
+func writeAuthData(user User) error {
+ real, err := osutil.RealUser()
+ if err != nil {
+ return err
+ }
+
+ uid, err := strconv.Atoi(real.Uid)
+ if err != nil {
+ return err
+ }
+
+ gid, err := strconv.Atoi(real.Gid)
+ if err != nil {
+ return err
+ }
+
+ targetFile := storeAuthDataFilename(real.HomeDir)
+
+ if err := osutil.MkdirAllChown(filepath.Dir(targetFile), 0700, uid, gid); err != nil {
+ return err
+ }
+
+ outStr, err := json.Marshal(user)
+ if err != nil {
+ return nil
+ }
+
+ return osutil.AtomicWriteFileChown(targetFile, []byte(outStr), 0600, 0, uid, gid)
+}
+
+// readAuthData reads previously written authentication details
+func readAuthData() (*User, error) {
+ sourceFile := storeAuthDataFilename("")
+ f, err := os.Open(sourceFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ var user User
+ dec := json.NewDecoder(f)
+ if err := dec.Decode(&user); err != nil {
+ return nil, err
+ }
+
+ return &user, nil
+}
+
+// removeAuthData removes any previously written authentication details.
+func removeAuthData() error {
+ filename := storeAuthDataFilename("")
+ return os.Remove(filename)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/osutil"
+)
+
+func (cs *clientSuite) TestClientLogin(c *check.C) {
+ cs.rsp = `{"type": "sync", "result":
+ {"username": "the-user-name",
+ "macaroon": "the-root-macaroon",
+ "discharges": ["discharge-macaroon"]}}`
+
+ outfile := filepath.Join(c.MkDir(), "json")
+ os.Setenv(client.TestAuthFileEnvKey, outfile)
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ c.Assert(cs.cli.LoggedInUser(), check.IsNil)
+
+ user, err := cs.cli.Login("username", "pass", "")
+ c.Check(err, check.IsNil)
+ c.Check(user, check.DeepEquals, &client.User{
+ Username: "the-user-name",
+ Macaroon: "the-root-macaroon",
+ Discharges: []string{"discharge-macaroon"}})
+
+ c.Assert(cs.cli.LoggedInUser(), check.Not(check.IsNil))
+
+ c.Check(osutil.FileExists(outfile), check.Equals, true)
+ content, err := ioutil.ReadFile(outfile)
+ c.Check(err, check.IsNil)
+ c.Check(string(content), check.Equals, `{"username":"the-user-name","macaroon":"the-root-macaroon","discharges":["discharge-macaroon"]}`)
+}
+
+func (cs *clientSuite) TestClientLoginError(c *check.C) {
+ cs.rsp = `{
+ "result": {},
+ "status": "Bad Request",
+ "status-code": 400,
+ "type": "error"
+ }`
+
+ outfile := filepath.Join(c.MkDir(), "json")
+ os.Setenv(client.TestAuthFileEnvKey, outfile)
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ user, err := cs.cli.Login("username", "pass", "")
+
+ c.Check(user, check.IsNil)
+ c.Check(err, check.NotNil)
+
+ c.Check(osutil.FileExists(outfile), check.Equals, false)
+}
+
+func (cs *clientSuite) TestClientLogout(c *check.C) {
+ cs.rsp = `{"type": "sync", "result": {}}`
+
+ outfile := filepath.Join(c.MkDir(), "json")
+ os.Setenv(client.TestAuthFileEnvKey, outfile)
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ err := ioutil.WriteFile(outfile, []byte(`{"macaroon":"macaroon","discharges":["discharged"]}`), 0600)
+ c.Assert(err, check.IsNil)
+
+ err = cs.cli.Logout()
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/logout"))
+
+ c.Check(osutil.FileExists(outfile), check.Equals, false)
+}
+
+func (cs *clientSuite) TestWriteAuthData(c *check.C) {
+ outfile := filepath.Join(c.MkDir(), "json")
+ os.Setenv(client.TestAuthFileEnvKey, outfile)
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ authData := client.User{
+ Macaroon: "macaroon",
+ Discharges: []string{"discharge"},
+ }
+ err := client.TestWriteAuth(authData)
+ c.Assert(err, check.IsNil)
+
+ c.Check(osutil.FileExists(outfile), check.Equals, true)
+ content, err := ioutil.ReadFile(outfile)
+ c.Check(err, check.IsNil)
+ c.Check(string(content), check.Equals, `{"macaroon":"macaroon","discharges":["discharge"]}`)
+}
+
+func (cs *clientSuite) TestReadAuthData(c *check.C) {
+ outfile := filepath.Join(c.MkDir(), "json")
+ os.Setenv(client.TestAuthFileEnvKey, outfile)
+ defer os.Unsetenv(client.TestAuthFileEnvKey)
+
+ authData := client.User{
+ Macaroon: "macaroon",
+ Discharges: []string{"discharge"},
+ }
+ err := client.TestWriteAuth(authData)
+ c.Assert(err, check.IsNil)
+
+ readUser, err := client.TestReadAuth()
+ c.Assert(err, check.IsNil)
+ c.Check(readUser, check.DeepEquals, &authData)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "time"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// Snap holds the data for a snap as obtained from snapd.
+type Snap struct {
+ ID string `json:"id"`
+ Summary string `json:"summary"`
+ Description string `json:"description"`
+ DownloadSize int64 `json:"download-size"`
+ Icon string `json:"icon"`
+ InstalledSize int64 `json:"installed-size"`
+ InstallDate time.Time `json:"install-date"`
+ Name string `json:"name"`
+ Developer string `json:"developer"`
+ Status string `json:"status"`
+ Type string `json:"type"`
+ Version string `json:"version"`
+ Channel string `json:"channel"`
+ TrackingChannel string `json:"tracking-channel"`
+ Revision snap.Revision `json:"revision"`
+ Confinement string `json:"confinement"`
+ Private bool `json:"private"`
+ DevMode bool `json:"devmode"`
+ JailMode bool `json:"jailmode"`
+ TryMode bool `json:"trymode"`
+ Apps []AppInfo `json:"apps"`
+ Broken string `json:"broken"`
+
+ Prices map[string]float64 `json:"prices"`
+ Screenshots []Screenshot `json:"screenshots"`
+
+ Channels map[string]*snap.ChannelSnapInfo `json:"channels"`
+}
+
+type AppInfo struct {
+ Name string `json:"name"`
+ Daemon string `json:"daemon"`
+ Aliases []string `json:"aliases"`
+}
+
+type Screenshot struct {
+ URL string `json:"url"`
+ Width int64 `json:"width,omitempty"`
+ Height int64 `json:"height,omitempty"`
+}
+
+// Statuses and types a snap may have.
+const (
+ StatusAvailable = "available"
+ StatusInstalled = "installed"
+ StatusActive = "active"
+ StatusRemoved = "removed"
+ StatusPriced = "priced"
+
+ TypeApp = "app"
+ TypeKernel = "kernel"
+ TypeGadget = "gadget"
+ TypeOS = "os"
+
+ StrictConfinement = "strict"
+ DevModeConfinement = "devmode"
+ ClassicConfinement = "classic"
+)
+
+type ResultInfo struct {
+ SuggestedCurrency string `json:"suggested-currency"`
+}
+
+// FindOptions supports exactly one of the following options:
+// - Refresh: only return snaps that are refreshable
+// - Private: return snaps that are private
+// - Query: only return snaps that match the query string
+type FindOptions struct {
+ Refresh bool
+ Private bool
+ Prefix bool
+ Query string
+ Section string
+}
+
+var ErrNoSnapsInstalled = errors.New("no snaps installed")
+
+type ListOptions struct {
+ All bool
+}
+
+// List returns the list of all snaps installed on the system
+// with names in the given list; if the list is empty, all snaps.
+func (client *Client) List(names []string, opts *ListOptions) ([]*Snap, error) {
+ if opts == nil {
+ opts = &ListOptions{}
+ }
+
+ q := make(url.Values)
+ if opts.All {
+ q.Add("select", "all")
+ }
+
+ snaps, _, err := client.snapsFromPath("/v2/snaps", q)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(snaps) == 0 {
+ return nil, ErrNoSnapsInstalled
+ }
+
+ if len(names) == 0 {
+ return snaps, nil
+ }
+
+ wanted := make(map[string]bool, len(names))
+ for _, name := range names {
+ wanted[name] = true
+ }
+
+ var result []*Snap
+ for _, snap := range snaps {
+ if wanted[snap.Name] {
+ result = append(result, snap)
+ }
+ }
+
+ return result, nil
+}
+
+// Sections returns the list of existing snap sections in the store
+func (client *Client) Sections() ([]string, error) {
+ var sections []string
+ _, err := client.doSync("GET", "/v2/sections", nil, nil, nil, §ions)
+ if err != nil {
+ return nil, fmt.Errorf("cannot get snap sections: %s", err)
+ }
+ return sections, nil
+}
+
+// Find returns a list of snaps available for install from the
+// store for this system and that match the query
+func (client *Client) Find(opts *FindOptions) ([]*Snap, *ResultInfo, error) {
+ if opts == nil {
+ opts = &FindOptions{}
+ }
+
+ q := url.Values{}
+ if opts.Prefix {
+ q.Set("name", opts.Query+"*")
+ } else {
+ q.Set("q", opts.Query)
+ }
+ switch {
+ case opts.Refresh && opts.Private:
+ return nil, nil, fmt.Errorf("cannot specify refresh and private together")
+ case opts.Refresh:
+ q.Set("select", "refresh")
+ case opts.Private:
+ q.Set("select", "private")
+ }
+ if opts.Section != "" {
+ q.Set("section", opts.Section)
+ }
+
+ return client.snapsFromPath("/v2/find", q)
+}
+
+func (client *Client) FindOne(name string) (*Snap, *ResultInfo, error) {
+ q := url.Values{}
+ q.Set("name", name)
+
+ snaps, ri, err := client.snapsFromPath("/v2/find", q)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot find snap %q: %s", name, err)
+ }
+
+ if len(snaps) == 0 {
+ return nil, nil, fmt.Errorf("cannot find snap %q", name)
+ }
+
+ return snaps[0], ri, nil
+}
+
+func (client *Client) snapsFromPath(path string, query url.Values) ([]*Snap, *ResultInfo, error) {
+ var snaps []*Snap
+ ri, err := client.doSync("GET", path, query, nil, nil, &snaps)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot list snaps: %s", err)
+ }
+ return snaps, ri, nil
+}
+
+// Snap returns the most recently published revision of the snap with the
+// provided name.
+func (client *Client) Snap(name string) (*Snap, *ResultInfo, error) {
+ var snap *Snap
+ path := fmt.Sprintf("/v2/snaps/%s", name)
+ ri, err := client.doSync("GET", path, nil, nil, nil, &snap)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot retrieve snap %q: %s", name, err)
+ }
+ return snap, ri, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "fmt"
+ "net/url"
+ "time"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+func (cs *clientSuite) TestClientSnapsCallsEndpoint(c *check.C) {
+ _, _ = cs.cli.List(nil, nil)
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{})
+}
+
+func (cs *clientSuite) TestClientFindRefreshSetsQuery(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{
+ Refresh: true,
+ })
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
+ "q": []string{""}, "select": []string{"refresh"},
+ })
+}
+
+func (cs *clientSuite) TestClientFindRefreshSetsQueryWithSec(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{
+ Refresh: true,
+ Section: "mysection",
+ })
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
+ "q": []string{""}, "section": []string{"mysection"}, "select": []string{"refresh"},
+ })
+}
+
+func (cs *clientSuite) TestClientFindWithSectionSetsQuery(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{
+ Section: "mysection",
+ })
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
+ "q": []string{""}, "section": []string{"mysection"},
+ })
+}
+
+func (cs *clientSuite) TestClientFindPrivateSetsQuery(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{
+ Private: true,
+ })
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+
+ c.Check(cs.req.URL.Query().Get("select"), check.Equals, "private")
+}
+
+func (cs *clientSuite) TestClientSnapsInvalidSnapsJSON(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": "not a list of snaps"
+ }`
+ _, err := cs.cli.List(nil, nil)
+ c.Check(err, check.ErrorMatches, `.*cannot unmarshal.*`)
+}
+
+func (cs *clientSuite) TestClientNoSnaps(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": [],
+ "suggested-currency": "GBP"
+ }`
+ _, err := cs.cli.List(nil, nil)
+ c.Check(err, check.Equals, client.ErrNoSnapsInstalled)
+ _, err = cs.cli.List([]string{"foo"}, nil)
+ c.Check(err, check.Equals, client.ErrNoSnapsInstalled)
+}
+
+func (cs *clientSuite) TestClientSnaps(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": [{
+ "id": "funky-snap-id",
+ "summary": "salutation snap",
+ "description": "hello-world",
+ "download-size": 22212,
+ "icon": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ "installed-size": -1,
+ "name": "hello-world",
+ "developer": "canonical",
+ "resource": "/v2/snaps/hello-world.canonical",
+ "status": "available",
+ "type": "app",
+ "version": "1.0.18",
+ "confinement": "strict",
+ "private": true
+ }],
+ "suggested-currency": "GBP"
+ }`
+ applications, err := cs.cli.List(nil, nil)
+ c.Check(err, check.IsNil)
+ c.Check(applications, check.DeepEquals, []*client.Snap{{
+ ID: "funky-snap-id",
+ Summary: "salutation snap",
+ Description: "hello-world",
+ DownloadSize: 22212,
+ Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ InstalledSize: -1,
+ Name: "hello-world",
+ Developer: "canonical",
+ Status: client.StatusAvailable,
+ Type: client.TypeApp,
+ Version: "1.0.18",
+ Confinement: client.StrictConfinement,
+ Private: true,
+ DevMode: false,
+ }})
+ otherApps, err := cs.cli.List([]string{"foo"}, nil)
+ c.Check(err, check.IsNil)
+ c.Check(otherApps, check.HasLen, 0)
+}
+
+func (cs *clientSuite) TestClientFilterSnaps(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{Query: "foo"})
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "q=foo")
+}
+
+func (cs *clientSuite) TestClientFindPrefix(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{Query: "foo", Prefix: true})
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo%2A") // 2A is `*`
+}
+
+func (cs *clientSuite) TestClientFindOne(c *check.C) {
+ _, _, _ = cs.cli.FindOne("foo")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo")
+}
+
+const (
+ pkgName = "chatroom"
+)
+
+func (cs *clientSuite) TestClientSnap(c *check.C) {
+ // example data obtained via
+ // printf "GET /v2/find?name=test-snapd-tools HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket|grep '{'|python3 -m json.tool
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "id": "funky-snap-id",
+ "summary": "bla bla",
+ "description": "WebRTC Video chat server for Snappy",
+ "download-size": 6930947,
+ "icon": "/v2/icons/chatroom.ogra/icon",
+ "installed-size": 18976651,
+ "install-date": "2016-01-02T15:04:05Z",
+ "name": "chatroom",
+ "developer": "ogra",
+ "resource": "/v2/snaps/chatroom.ogra",
+ "status": "active",
+ "type": "app",
+ "version": "0.1-8",
+ "confinement": "strict",
+ "private": true,
+ "devmode": true,
+ "trymode": true,
+ "screenshots": [
+ {"url":"http://example.com/shot1.png", "width":640, "height":480},
+ {"url":"http://example.com/shot2.png"}
+ ]
+ }
+ }`
+ pkg, _, err := cs.cli.Snap(pkgName)
+ c.Assert(cs.req.Method, check.Equals, "GET")
+ c.Assert(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName))
+ c.Assert(err, check.IsNil)
+ c.Assert(pkg, check.DeepEquals, &client.Snap{
+ ID: "funky-snap-id",
+ Summary: "bla bla",
+ Description: "WebRTC Video chat server for Snappy",
+ DownloadSize: 6930947,
+ Icon: "/v2/icons/chatroom.ogra/icon",
+ InstalledSize: 18976651,
+ InstallDate: time.Date(2016, 1, 2, 15, 4, 5, 0, time.UTC),
+ Name: "chatroom",
+ Developer: "ogra",
+ Status: client.StatusActive,
+ Type: client.TypeApp,
+ Version: "0.1-8",
+ Confinement: client.StrictConfinement,
+ Private: true,
+ DevMode: true,
+ TryMode: true,
+ Screenshots: []client.Screenshot{
+ {URL: "http://example.com/shot1.png", Width: 640, Height: 480},
+ {URL: "http://example.com/shot2.png"},
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "os"
+ "path/filepath"
+ "strconv"
+)
+
+type SnapOptions struct {
+ Channel string `json:"channel,omitempty"`
+ Revision string `json:"revision,omitempty"`
+ DevMode bool `json:"devmode,omitempty"`
+ JailMode bool `json:"jailmode,omitempty"`
+ Classic bool `json:"classic,omitempty"`
+ Dangerous bool `json:"dangerous,omitempty"`
+ IgnoreValidation bool `json:"ignore-validation,omitempty"`
+}
+
+type actionData struct {
+ Action string `json:"action"`
+ Name string `json:"name,omitempty"`
+ SnapPath string `json:"snap-path,omitempty"`
+ *SnapOptions
+}
+
+type multiActionData struct {
+ Action string `json:"action"`
+ Snaps []string `json:"snaps,omitempty"`
+}
+
+// Install adds the snap with the given name from the given channel (or
+// the system default channel if not).
+func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("install", name, options)
+}
+
+func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) {
+ return client.doMultiSnapAction("install", names, options)
+}
+
+// Remove removes the snap with the given name.
+func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("remove", name, options)
+}
+
+func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) {
+ return client.doMultiSnapAction("remove", names, options)
+}
+
+// Refresh refreshes the snap with the given name (switching it to track
+// the given channel if given).
+func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("refresh", name, options)
+}
+
+func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) {
+ return client.doMultiSnapAction("refresh", names, options)
+}
+
+func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("enable", name, options)
+}
+
+func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("disable", name, options)
+}
+
+// Revert rolls the snap back to the previous on-disk state
+func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("revert", name, options)
+}
+
+var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file")
+
+func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) {
+ if options != nil && options.Dangerous {
+ return "", ErrDangerousNotApplicable
+ }
+ action := actionData{
+ Action: actionName,
+ SnapOptions: options,
+ }
+ data, err := json.Marshal(&action)
+ if err != nil {
+ return "", fmt.Errorf("cannot marshal snap action: %s", err)
+ }
+ path := fmt.Sprintf("/v2/snaps/%s", snapName)
+
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+
+ return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data))
+}
+
+func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) {
+ if options != nil {
+ return "", fmt.Errorf("cannot use options for multi-action") // (yet)
+ }
+ action := multiActionData{
+ Action: actionName,
+ Snaps: snaps,
+ }
+ data, err := json.Marshal(&action)
+ if err != nil {
+ return "", fmt.Errorf("cannot marshal multi-snap action: %s", err)
+ }
+
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+
+ return client.doAsync("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data))
+}
+
+// InstallPath sideloads the snap with the given path, returning the UUID
+// of the background operation upon success.
+func (client *Client) InstallPath(path string, options *SnapOptions) (changeID string, err error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return "", fmt.Errorf("cannot open: %q", path)
+ }
+
+ action := actionData{
+ Action: "install",
+ SnapPath: path,
+ SnapOptions: options,
+ }
+
+ pr, pw := io.Pipe()
+ mw := multipart.NewWriter(pw)
+ go sendSnapFile(path, f, pw, mw, &action)
+
+ headers := map[string]string{
+ "Content-Type": mw.FormDataContentType(),
+ }
+
+ return client.doAsync("POST", "/v2/snaps", nil, headers, pr)
+}
+
+// Try
+func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) {
+ if options == nil {
+ options = &SnapOptions{}
+ }
+ if options.Dangerous {
+ return "", ErrDangerousNotApplicable
+ }
+
+ buf := bytes.NewBuffer(nil)
+ mw := multipart.NewWriter(buf)
+ mw.WriteField("action", "try")
+ mw.WriteField("snap-path", path)
+ mw.WriteField("devmode", strconv.FormatBool(options.DevMode))
+ mw.WriteField("jailmode", strconv.FormatBool(options.JailMode))
+ mw.Close()
+
+ headers := map[string]string{
+ "Content-Type": mw.FormDataContentType(),
+ }
+
+ return client.doAsync("POST", "/v2/snaps", nil, headers, buf)
+}
+
+func sendSnapFile(snapPath string, snapFile *os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) {
+ defer snapFile.Close()
+
+ if action.SnapOptions == nil {
+ action.SnapOptions = &SnapOptions{}
+ }
+ errs := []error{
+ mw.WriteField("action", action.Action),
+ mw.WriteField("name", action.Name),
+ mw.WriteField("snap-path", action.SnapPath),
+ mw.WriteField("channel", action.Channel),
+ mw.WriteField("devmode", strconv.FormatBool(action.DevMode)),
+ mw.WriteField("jailmode", strconv.FormatBool(action.JailMode)),
+ mw.WriteField("classic", strconv.FormatBool(action.Classic)),
+ mw.WriteField("dangerous", strconv.FormatBool(action.Dangerous)),
+ }
+ for _, err := range errs {
+ if err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+ }
+
+ fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath))
+ if err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+
+ _, err = io.Copy(fw, snapFile)
+ if err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+
+ mw.Close()
+ pw.Close()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime"
+ "mime/multipart"
+ "path/filepath"
+ "strconv"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+var chanName = "achan"
+
+var ops = []struct {
+ op func(*client.Client, string, *client.SnapOptions) (string, error)
+ action string
+}{
+ {(*client.Client).Install, "install"},
+ {(*client.Client).Refresh, "refresh"},
+ {(*client.Client).Remove, "remove"},
+ {(*client.Client).Revert, "revert"},
+ {(*client.Client).Enable, "enable"},
+ {(*client.Client).Disable, "disable"},
+}
+
+var multiOps = []struct {
+ op func(*client.Client, []string, *client.SnapOptions) (string, error)
+ action string
+}{
+ {(*client.Client).RefreshMany, "refresh"},
+ {(*client.Client).InstallMany, "install"},
+ {(*client.Client).RemoveMany, "remove"},
+}
+
+func (cs *clientSuite) TestClientOpSnapServerError(c *check.C) {
+ cs.err = errors.New("fail")
+ for _, s := range ops {
+ _, err := s.op(cs.cli, pkgName, nil)
+ c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientMultiOpSnapServerError(c *check.C) {
+ cs.err = errors.New("fail")
+ for _, s := range multiOps {
+ _, err := s.op(cs.cli, nil, nil)
+ c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) {
+ cs.rsp = `{"type": "error", "status": "potatoes"}`
+ for _, s := range ops {
+ _, err := s.op(cs.cli, pkgName, nil)
+ c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) {
+ cs.rsp = `{"type": "error", "status": "potatoes"}`
+ for _, s := range multiOps {
+ _, err := s.op(cs.cli, nil, nil)
+ c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) {
+ cs.rsp = `{"type": "what"}`
+ for _, s := range ops {
+ _, err := s.op(cs.cli, pkgName, nil)
+ c.Check(err, check.ErrorMatches, `.*expected async response for "POST" on "/v2/snaps/`+pkgName+`", got "what"`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientOpSnapNotAccepted(c *check.C) {
+ cs.rsp = `{
+ "status-code": 200,
+ "type": "async"
+ }`
+ for _, s := range ops {
+ _, err := s.op(cs.cli, pkgName, nil)
+ c.Check(err, check.ErrorMatches, `.*operation not accepted`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) {
+ cs.rsp = `{
+ "status-code": 202,
+ "type": "async"
+ }`
+ for _, s := range ops {
+ _, err := s.op(cs.cli, pkgName, nil)
+ c.Assert(err, check.ErrorMatches, `.*response without change reference.*`, check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientOpSnap(c *check.C) {
+ cs.rsp = `{
+ "change": "d728",
+ "status-code": 202,
+ "type": "async"
+ }`
+ for _, s := range ops {
+ id, err := s.op(cs.cli, pkgName, &client.SnapOptions{Channel: chanName})
+ c.Assert(err, check.IsNil)
+
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action))
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil, check.Commentf(s.action))
+ jsonBody := make(map[string]string)
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, check.IsNil, check.Commentf(s.action))
+ c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action))
+ c.Check(jsonBody["channel"], check.Equals, chanName, check.Commentf(s.action))
+ c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action))
+
+ c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName), check.Commentf(s.action))
+ c.Check(id, check.Equals, "d728", check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) {
+ cs.rsp = `{
+ "change": "d728",
+ "status-code": 202,
+ "type": "async"
+ }`
+ for _, s := range multiOps {
+ id, err := s.op(cs.cli, []string{pkgName}, nil)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action))
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil, check.Commentf(s.action))
+ jsonBody := make(map[string]interface{})
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, check.IsNil, check.Commentf(s.action))
+ c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action))
+ c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}, check.Commentf(s.action))
+ c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action))
+
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", check.Commentf(s.action))
+ c.Check(id, check.Equals, "d728", check.Commentf(s.action))
+ }
+}
+
+func (cs *clientSuite) TestClientOpInstallPath(c *check.C) {
+ cs.rsp = `{
+ "change": "66b3",
+ "status-code": 202,
+ "type": "async"
+ }`
+ bodyData := []byte("snap-data")
+
+ snap := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snap, bodyData, 0644)
+ c.Assert(err, check.IsNil)
+
+ id, err := cs.cli.InstallPath(snap, nil)
+ c.Assert(err, check.IsNil)
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
+ c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
+ c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\nfalse\r\n.*")
+
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"))
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*")
+ c.Check(id, check.Equals, "66b3")
+}
+
+func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) {
+ cs.rsp = `{
+ "change": "66b3",
+ "status-code": 202,
+ "type": "async"
+ }`
+ bodyData := []byte("snap-data")
+
+ snap := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snap, bodyData, 0644)
+ c.Assert(err, check.IsNil)
+
+ opts := client.SnapOptions{
+ Dangerous: true,
+ }
+
+ // InstallPath takes Dangerous
+ _, err = cs.cli.InstallPath(snap, &opts)
+ c.Assert(err, check.IsNil)
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*")
+
+ // Install does not (and gives us a clear error message)
+ _, err = cs.cli.Install("foo", &opts)
+ c.Assert(err, check.Equals, client.ErrDangerousNotApplicable)
+
+ // nor does InstallMany (whether it fails because any option
+ // at all was provided, or because dangerous was provided, is
+ // unimportant)
+ _, err = cs.cli.InstallMany([]string{"foo"}, &opts)
+ c.Assert(err, check.NotNil)
+}
+
+func formToMap(c *check.C, mr *multipart.Reader) map[string]string {
+ formData := map[string]string{}
+ for {
+ p, err := mr.NextPart()
+ if err == io.EOF {
+ break
+ }
+ c.Assert(err, check.IsNil)
+ slurp, err := ioutil.ReadAll(p)
+ c.Assert(err, check.IsNil)
+ formData[p.FormName()] = string(slurp)
+ }
+ return formData
+}
+
+func (cs *clientSuite) TestClientOpTryMode(c *check.C) {
+ cs.rsp = `{
+ "change": "66b3",
+ "status-code": 202,
+ "type": "async"
+ }`
+ snapdir := filepath.Join(c.MkDir(), "/some/path")
+
+ for _, opts := range []*client.SnapOptions{
+ {DevMode: false, JailMode: false},
+ {DevMode: false, JailMode: true},
+ {DevMode: true, JailMode: true},
+ {DevMode: true, JailMode: false},
+ } {
+ id, err := cs.cli.Try(snapdir, opts)
+ c.Assert(err, check.IsNil)
+
+ // ensure we send the right form-data
+ _, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type"))
+ c.Assert(err, check.IsNil)
+ mr := multipart.NewReader(cs.req.Body, params["boundary"])
+ formData := formToMap(c, mr)
+ c.Check(formData, check.DeepEquals, map[string]string{
+ "action": "try",
+ "snap-path": snapdir,
+ "devmode": strconv.FormatBool(opts.DevMode),
+ "jailmode": strconv.FormatBool(opts.JailMode),
+ })
+
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"))
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*")
+ c.Check(id, check.Equals, "66b3")
+ }
+}
+
+func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) {
+ snapdir := filepath.Join(c.MkDir(), "/some/path")
+
+ _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true})
+ c.Assert(err, check.Equals, client.ErrDangerousNotApplicable)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
+// SnapCtlOptions holds the various options with which snapctl is invoked.
+type SnapCtlOptions struct {
+ // ContextID is a string used to determine the context of this call (e.g.
+ // which context and handler should be used, etc.)
+ ContextID string `json:"context-id"`
+
+ // Args contains a list of parameters to use for this invocation.
+ Args []string `json:"args"`
+}
+
+type snapctlOutput struct {
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+}
+
+// RunSnapctl requests a snapctl run for the given options.
+func (client *Client) RunSnapctl(options *SnapCtlOptions) (stdout, stderr []byte, err error) {
+ b, err := json.Marshal(options)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot marshal options: %s", err)
+ }
+
+ var output snapctlOutput
+ _, err = client.doSync("POST", "/v2/snapctl", nil, nil, bytes.NewReader(b), &output)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return []byte(output.Stdout), []byte(output.Stderr), nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+
+ "github.com/snapcore/snapd/client"
+
+ "gopkg.in/check.v1"
+)
+
+func (cs *clientSuite) TestClientRunSnapctlCallsEndpoint(c *check.C) {
+ options := &client.SnapCtlOptions{
+ ContextID: "1234ABCD",
+ Args: []string{"foo", "bar"},
+ }
+ cs.cli.RunSnapctl(options)
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/snapctl")
+}
+
+func (cs *clientSuite) TestClientRunSnapctl(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {
+ "stdout": "test stdout",
+ "stderr": "test stderr"
+ }
+ }`
+
+ options := &client.SnapCtlOptions{
+ ContextID: "1234ABCD",
+ Args: []string{"foo", "bar"},
+ }
+
+ stdout, stderr, err := cs.cli.RunSnapctl(options)
+ c.Assert(err, check.IsNil)
+ c.Check(string(stdout), check.Equals, "test stdout")
+ c.Check(string(stderr), check.Equals, "test stderr")
+
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "context-id": "1234ABCD",
+ "args": []interface{}{"foo", "bar"},
+ })
+}
--- /dev/null
+SUBDIRS = snap-confine
+EXTRA_DIST = VERSION
+
+.PHONY: check-syntax
+check-syntax:
+ shellcheck --format=gcc snap-confine/spread-tests/spread-prepare.sh
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package cmd
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "syscall"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/strutil"
+)
+
+// The SNAP_REEXEC environment variable controls whether the command
+// will attempt to re-exec itself from inside an ubuntu-core snap
+// present on the system. If not present in the environ it's assumed
+// to be set to 1 (do re-exec); that is: set it to 0 to disable.
+const key = "SNAP_REEXEC"
+
+// newCore is the place to look for the core snap; everything in this
+// location will be new enough to re-exec into.
+const newCore = "/snap/core/current"
+
+// oldCore is the previous location of the core snap. Only things
+// newer than minOldRevno will be ok to re-exec into.
+const oldCore = "/snap/ubuntu-core/current"
+
+// ExecInCoreSnap makes sure you're executing the binary that ships in
+// the core snap.
+func ExecInCoreSnap() {
+ if !release.OnClassic {
+ // you're already the real deal, natch
+ return
+ }
+
+ // should we re-exec? no option in the environment means yes
+ if !osutil.GetenvBool(key, true) {
+ return
+ }
+
+ exe, err := os.Readlink("/proc/self/exe")
+ if err != nil {
+ return
+ }
+
+ corePath := newCore
+ full := filepath.Join(newCore, exe)
+ if !osutil.FileExists(full) {
+ corePath = oldCore
+ full = filepath.Join(oldCore, exe)
+ if !osutil.FileExists(full) {
+ return
+ }
+ }
+
+ // ensure we do not re-exec into an older version of snapd, look
+ // for info file and ignore version of core that do not yet have
+ // it
+ fullInfo := filepath.Join(corePath, "/usr/lib/snapd/info")
+ if !osutil.FileExists(fullInfo) {
+ logger.Debugf("not restarting into %q (no version info): older than %q (%s)", full, exe, Version)
+ return
+ }
+ content, err := ioutil.ReadFile(fullInfo)
+ if err != nil {
+ logger.Noticef("cannot read info file %q: %s", fullInfo, err)
+ return
+ }
+ ver := regexp.MustCompile("(?m)^VERSION=(.*)$").FindStringSubmatch(string(content))
+ if len(ver) != 2 {
+ logger.Noticef("cannot find version information in %q", content)
+ }
+ // > 0 means our Version is bigger than the version of snapd in core
+ res, err := strutil.VersionCompare(Version, ver[1])
+ if err != nil {
+ logger.Debugf("cannot version compare %q and %q: %s", Version, ver[1], res)
+ return
+ }
+ if res > 0 {
+ logger.Debugf("not restarting into %q (%s): older than %q (%s)", full, ver, exe, Version)
+ return
+ }
+
+ logger.Debugf("restarting into %q", full)
+
+ env := append(os.Environ(), key+"=0")
+ panic(syscall.Exec(full, os.Args, env))
+}
--- /dev/null
+AC_PREREQ([2.69])
+AC_INIT([snap-confine], m4_esyscmd_s([cat VERSION]), [snapcraft@lists.ubuntu.com])
+AC_CONFIG_SRCDIR([snap-confine/snap-confine.c])
+AC_CONFIG_HEADERS([config.h])
+AC_USE_SYSTEM_EXTENSIONS
+AM_INIT_AUTOMAKE([foreign])
+AM_MAINTAINER_MODE([enable])
+
+# Checks for programs.
+AC_PROG_CC_C99
+AC_PROG_CPP
+AC_PROG_INSTALL
+AC_PROG_MAKE_SET
+AC_PROG_RANLIB
+
+AC_LANG([C])
+# Checks for libraries.
+
+# Checks for header files.
+AC_CHECK_HEADERS([fcntl.h limits.h stdlib.h string.h sys/mount.h unistd.h])
+
+# Checks for typedefs, structures, and compiler characteristics.
+AC_CHECK_HEADER_STDBOOL
+AC_TYPE_UID_T
+AC_TYPE_MODE_T
+AC_TYPE_PID_T
+AC_TYPE_SIZE_T
+
+# Checks for library functions.
+AC_FUNC_CHOWN
+AC_FUNC_ERROR_AT_LINE
+AC_FUNC_FORK
+AC_FUNC_STRNLEN
+AC_CHECK_FUNCS([mkdir regcomp setenv strdup strerror secure_getenv])
+
+AC_ARG_WITH([unit-tests],
+ AC_HELP_STRING([--without-unit-tests], [do not build unit test programs]),
+ [case "${withval}" in
+ yes) with_unit_tests=yes ;;
+ no) with_unit_tests=no ;;
+ *) AC_MSG_ERROR([bad value ${withval} for --without-unit-tests])
+ esac], [with_unit_tests=yes])
+AM_CONDITIONAL([WITH_UNIT_TESTS], [test "x$with_unit_tests" = "xyes"])
+
+# Allow to build without apparmor support by calling:
+# ./configure --disable-apparmor
+# This makes it possible to run snaps in devmode on almost any host,
+# regardless of the kernel version.
+AC_ARG_ENABLE([apparmor],
+ AS_HELP_STRING([--disable-apparmor], [Disable apparmor support]),
+ [case "${enableval}" in
+ yes) enable_apparmor=yes ;;
+ no) enable_apparmor=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --disable-apparmor])
+ esac], [enable_apparmor=yes])
+AM_CONDITIONAL([APPARMOR], [test "x$enable_apparmor" = "xyes"])
+
+# Allow to build without seccomp support by calling:
+# ./configure --disable-seccomp
+# This is separate because seccomp support is generally very good and it
+# provides useful confinement for unsafe system calls.
+AC_ARG_ENABLE([seccomp],
+ AS_HELP_STRING([--disable-seccomp], [Disable seccomp support]),
+ [case "${enableval}" in
+ yes) enable_seccomp=yes ;;
+ no) enable_seccomp=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --disable-seccomp])
+ esac], [enable_seccomp=yes])
+AM_CONDITIONAL([SECCOMP], [test "x$enable_seccomp" = "xyes"])
+
+# Enable older tests only when confinement is enabled and we're building for PC
+# The tests are of smaller value as we port more and more tests to spread.
+AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && test "x$enable_seccomp" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))])
+
+# Check for glib that we use for unit testing
+AS_IF([test "x$with_unit_tests" = "xyes"], [
+ PKG_CHECK_MODULES([GLIB], [glib-2.0])
+])
+
+# Check if seccomp userspace library is available
+AS_IF([test "x$enable_seccomp" = "xyes"], [
+ PKG_CHECK_MODULES([SECCOMP], [libseccomp], [
+ AC_DEFINE([HAVE_SECCOMP], [1], [Build with seccomp support])])
+])
+
+# Check if apparmor userspace library is available.
+AS_IF([test "x$enable_apparmor" = "xyes"], [
+ PKG_CHECK_MODULES([APPARMOR], [libapparmor], [
+ AC_DEFINE([HAVE_APPARMOR], [1], [Build with apparmor support])])
+], [
+ AC_MSG_WARN([
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ X X
+ X Apparmor is disabled, all snaps will run in devmode X
+ X X
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX])
+])
+
+# Check if udev and libudev are available.
+# Those are now used unconditionally even if apparmor is disabled.
+PKG_CHECK_MODULES([LIBUDEV], [libudev])
+PKG_CHECK_MODULES([UDEV], [udev])
+
+# Enable special support for hosts with proprietary nvidia drivers on Ubuntu.
+AC_ARG_ENABLE([nvidia-ubuntu],
+ AS_HELP_STRING([--enable-nvidia-ubuntu], [Support for proprietary nvidia drivers (Ubuntu)]),
+ [case "${enableval}" in
+ yes) enable_nvidia_ubuntu=yes ;;
+ no) enable_nvidia_ubuntu=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-ubuntu])
+ esac], [enable_nvidia_ubuntu=no])
+AM_CONDITIONAL([NVIDIA_UBUNTU], [test "x$enable_nvidia_ubuntu" = "xyes"])
+
+AS_IF([test "x$enable_nvidia_ubuntu" = "xyes"], [
+ AC_DEFINE([NVIDIA_UBUNTU], [1],
+ [Support for proprietary nvidia drivers (Ubuntu)])])
+
+# Enable special support for hosts with proprietary nvidia drivers on Arch.
+AC_ARG_ENABLE([nvidia-arch],
+ AS_HELP_STRING([--enable-nvidia-arch], [Support for proprietary nvidia drivers (Arch)]),
+ [case "${enableval}" in
+ yes) enable_nvidia_arch=yes ;;
+ no) enable_nvidia_arch=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-arch])
+ esac], [enable_nvidia_arch=no])
+AM_CONDITIONAL([NVIDIA_ARCH], [test "x$enable_nvidia_arch" = "xyes"])
+
+AS_IF([test "x$enable_nvidia_arch" = "xyes"], [
+ AC_DEFINE([NVIDIA_ARCH], [1],
+ [Support for proprietary nvidia drivers (Arch)])])
+
+AC_ARG_ENABLE([merged-usr],
+ AS_HELP_STRING([--enable-merged-usr], [Enable support for merged /usr directory]),
+ [case "${enableval}" in
+ yes) enable_merged_usr=yes ;;
+ no) enable_merged_usr=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-merged-usr])
+ esac], [enable_merged_usr=no])
+AM_CONDITIONAL([MERGED_USR], [test "x$enable_merged_usr" = "xyes"])
+
+AS_IF([test "x$enable_merged_usr" = "xyes"], [
+ AC_DEFINE([MERGED_USR], [1],
+ [Support for merged /usr directory])])
+
+SNAP_MOUNT_DIR="/snap"
+AC_ARG_WITH([snap-mount-dir],
+ AS_HELP_STRING([--with-snap-mount-dir=DIR], [Use an alternate snap mount directory]),
+ [SNAP_MOUNT_DIR="$withval"])
+AC_SUBST(SNAP_MOUNT_DIR)
+AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR], "${SNAP_MOUNT_DIR}", [Location of the snap mount points])
+
+AC_ARG_ENABLE([caps-over-setuid],
+ AS_HELP_STRING([--enable-caps-over-setuid], [Use capabilities rather than setuid bit]),
+ [case "${enableval}" in
+ yes) enable_caps_over_setuid=yes ;;
+ no) enable_caps_over_setuid=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-caps-over-setuid])
+ esac], [enable_caps_over_setuid=no])
+AM_CONDITIONAL([CAPS_OVER_SETUID], [test "x$enable_caps_over_setuid" = "xyes"])
+
+AS_IF([test "x$enable_caps_over_setuid" = "xyes"], [
+ AC_DEFINE([CAPS_OVER_SETUID], [1],
+ [Use capabilities rather than setuid bit])])
+
+AC_PATH_PROG([HAVE_RST2MAN],[rst2man])
+AS_IF([test "x$HAVE_RST2MAN" = "x"], [AC_MSG_ERROR(["cannot find the rst2man tool, install python-docutils or similar"])])
+
+AC_CONFIG_FILES([Makefile snap-confine/Makefile snap-confine/tests/Makefile snap-confine/manpages/Makefile])
+AC_OUTPUT
--- /dev/null
+# add/remove snap package access to assigned devices
+TAG=="snap_*", RUN+="/lib/udev/snappy-app-dev $env{ACTION} $env{TAG} $devpath $major:$minor"
--- /dev/null
+SUBDIRS = manpages tests
+
+noinst_LIBRARIES = libsnap-confine-private.a
+
+libsnap_confine_private_a_SOURCES = \
+ error.h \
+ error.c \
+ utils.h \
+ utils.c \
+ secure-getenv.c \
+ secure-getenv.h
+
+libexec_PROGRAMS = snap-confine snap-discard-ns
+noinst_PROGRAMS = decode-mount-opts
+if WITH_UNIT_TESTS
+noinst_PROGRAMS += snap-confine-unit-tests
+endif
+
+decode_mount_opts_SOURCES = \
+ decode-mount-opts.c \
+ mount-opt.c \
+ mount-opt.h
+
+snap_discard_ns_SOURCES = \
+ ns-support.c \
+ ns-support.h \
+ apparmor-support.c \
+ apparmor-support.h \
+ cleanup-funcs.c \
+ cleanup-funcs.h \
+ mountinfo.c \
+ mountinfo.h \
+ snap-discard-ns.c
+snap_discard_ns_CFLAGS = -Wall -Werror $(AM_CFLAGS)
+snap_discard_ns_LDFLAGS = $(AM_LDFLAGS)
+snap_discard_ns_LDADD = libsnap-confine-private.a
+snap_discard_ns_CFLAGS += $(SECCOMP_CFLAGS)
+snap_discard_ns_LDADD += $(SECCOMP_LIBS)
+
+if APPARMOR
+snap_discard_ns_CFLAGS += $(APPARMOR_CFLAGS)
+snap_discard_ns_LDADD += $(APPARMOR_LIBS)
+endif
+
+snap_confine_SOURCES = \
+ snap-confine.c \
+ snap.c \
+ snap.h \
+ classic.c \
+ classic.h \
+ mount-support.c \
+ mount-support.h \
+ mount-support-nvidia.c \
+ mount-support-nvidia.h \
+ cleanup-funcs.c \
+ cleanup-funcs.h \
+ udev-support.c \
+ udev-support.h \
+ user-support.c \
+ user-support.h \
+ quirks.c \
+ quirks.h \
+ mount-opt.c \
+ mount-opt.h \
+ mountinfo.c \
+ mountinfo.h \
+ ns-support.c \
+ ns-support.h \
+ apparmor-support.c \
+ apparmor-support.h
+
+snap_confine_CFLAGS = -Wall -Werror $(AM_CFLAGS)
+snap_confine_LDFLAGS = $(AM_LDFLAGS)
+snap_confine_LDADD = libsnap-confine-private.a
+snap_confine_CFLAGS += $(LIBUDEV_CFLAGS)
+snap_confine_LDADD += $(LIBUDEV_LIBS)
+
+# This is here to help fix rpmlint hardening issue.
+# https://en.opensuse.org/openSUSE:Packaging_checks#non-position-independent-executable
+snap_confine_CFLAGS += $(SUID_CFLAGS)
+snap_confine_LDFLAGS += $(SUID_LDFLAGS)
+
+if SECCOMP
+snap_confine_SOURCES += \
+ seccomp-support.c \
+ seccomp-support.h
+snap_confine_CFLAGS += $(SECCOMP_CFLAGS)
+snap_confine_LDADD += $(SECCOMP_LIBS)
+endif
+
+if APPARMOR
+snap_confine_CFLAGS += $(APPARMOR_CFLAGS)
+snap_confine_LDADD += $(APPARMOR_LIBS)
+endif
+
+if WITH_UNIT_TESTS
+snap_confine_unit_tests_SOURCES = \
+ unit-tests-main.c \
+ classic.c \
+ classic.h \
+ quirks.c \
+ quirks.h \
+ unit-tests.c \
+ unit-tests.h \
+ utils-test.c \
+ cleanup-funcs-test.c \
+ mount-support-test.c \
+ verify-executable-name-test.c \
+ mountinfo-test.c \
+ ns-support-test.c \
+ apparmor-support.c \
+ apparmor-support.h \
+ mount-opt-test.c \
+ error-test.c
+snap_confine_unit_tests_CFLAGS = $(snap_confine_CFLAGS) $(GLIB_CFLAGS)
+snap_confine_unit_tests_LDADD = $(snap_confine_LDADD) $(GLIB_LIBS)
+snap_confine_unit_tests_LDFLAGS = $(snap_confine_LDFLAGS)
+endif
+
+# Force particular coding style on all source and header files.
+.PHONY: check-syntax
+check-syntax:
+ @d=`mktemp -d`; \
+ trap 'rm -rf $d' EXIT; \
+ for f in $(wildcard $(srcdir)/*.c) $(wildcard $(srcdir)/*.h); do \
+ out="$$d/`basename $$f.out`"; \
+ echo "Checking $$f ... "; \
+ indent -linux "$$f" -o "$$out"; \
+ diff -Naur "$$f" "$$out" || exit 1; \
+ done;
+
+.PHONY: check-unit-tests
+check-unit-tests: snap-confine-unit-tests
+if WITH_UNIT_TESTS
+ ./snap-confine-unit-tests
+endif
+
+
+# Run check-syntax when checking
+# TODO: conver those to autotools-style tests later
+check: check-syntax check-unit-tests
+
+.PHONY: fmt
+fmt:
+ for f in $(wildcard $(srcdir)/*.c) $(wildcard $(srcdir)/*.h); do \
+ echo "Formatting $$f ... "; \
+ indent -linux "$$f"; \
+ done;
+
+EXTRA_DIST = PORTING 80-snappy-assign.rules snappy-app-dev snap-confine.apparmor.in
+
+snap-confine.apparmor: snap-confine.apparmor.in Makefile
+ sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@
+
+# NOTE: This makes distcheck fail but it is required for udev, so go figure.
+# http://www.gnu.org/software/automake/manual/automake.html#Hard_002dCoded-Install-Paths
+#
+# Install udev rules and the apparmor profile
+#
+# NOTE: the funky make functions here just convert /foo/bar/froz into foo.bar.froz
+# The inner subst replaces slashes with dots and the outer patsubst strips the leading dot
+#
+# NOTE: The 'void' directory *has to* be chmod 000
+install-data-local: snap-confine.apparmor
+ install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d
+ install -m 644 $(srcdir)/80-snappy-assign.rules $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d
+ install -d -m 755 $(DESTDIR)/etc/apparmor.d/
+ install -m 644 snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine
+ install -d -m 000 $(DESTDIR)/var/lib/snapd/void
+
+# Install support script for udev rules
+install-exec-local:
+ install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir)
+ install -m 755 $(srcdir)/snappy-app-dev $(DESTDIR)$(shell pkg-config udev --variable=udevdir)
+
+install-exec-hook:
+if CAPS_OVER_SETUID
+# Ensure that snap-confine has CAP_SYS_ADMIN capability
+ setcap cap_sys_admin=pe $(DESTDIR)$(libexecdir)/snap-confine
+else
+# Ensure that snap-confine is +s (setuid)
+ chmod 4755 $(DESTDIR)$(libexecdir)/snap-confine
+endif
+ install -d -m 755 $(DESTDIR)$(bindir)
+ ln -sf $(libexecdir)/snap-confine $(DESTDIR)$(bindir)/ubuntu-core-launcher
+
+# The hack target helps devlopers work on snap-confine on their live system by
+# installing a fresh copy of snap confine and the appropriate apparmor profile.
+.PHONY: hack
+hack: snap-confine snap-confine.apparmor
+ sudo install -D -m 4755 snap-confine $(DESTDIR)$(libexecdir)/snap-confine
+ sudo install -m 644 snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine
+ sudo apparmor_parser -r snap-confine.apparmor
--- /dev/null
+Welcome brave porters!
+
+This file is intended to guide you towards porting snappy (comprised of snapd
+and this project, snap-confine) to work on a new kernel. The confinement setup by
+snap-confine has several requirements on the kernel.
+
+TODO: list required patches (apparmor, seccomp)
+TODO: list required kernel configufation
+TODO: list minimum supported kernel version
+
+While you are working on porting those patches to your kernel of choice, you
+may configure snap-confine with --disable-security. This switch drops
+requirement on apparmor, seccomp and udev and reduces snap-confine to arrange
+the filesystem in a correct way for snaps to operate without really confining
+them in any way.
--- /dev/null
+= Mount namespace setup in snap-confine =
+
+This document provides a terse explanation of the mount setup using syscall
+traces to show precisely what is happening and show the difference between
+all snaps images and classic.
+
+Obtain traces with (ignoring select helps keep strace from hanging):
+$ sudo snap install hello-world
+$ sudo /usr/lib/snapd/snap-discard-ns hello-world
+$ sudo strace -f -vv -s8192 -o /tmp/trace.unshare -e trace='!select' /snap/bin/hello-world
+$ sudo strace -f -vv -s8192 -o /tmp/trace.setns -e trace='!select' /snap/bin/hello-world
+
+Examine /tmp/trace.unshare for initial mount namespace setup and
+/tmp/trace.setns for seeing how the mount namespace is reused on subsequent
+runs. Note that running /usr/lib/snapd/snap-discard-ns prior to running the
+command is required for creating the new mount namespace (otherwise the
+previous mount namespace will be reused).
+
+
+= Mount namespace setup in detail =
+Here are the steps snap-confine takes when setting up the mount namespace for a
+given snap:
+
+# Create the /run/snapd/ns directory to save off the mount namespace to be
+# shared on other app-invocations
+open("/", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 3
+mkdirat(3, "run", 0755) = -1 EEXIST (File exists)
+openat(3, "run", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 4
+mkdirat(4, "snapd", 0755) = -1 EEXIST (File exists)
+openat(4, "snapd", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 3
+mkdirat(3, "ns", 0755) = -1 EEXIST (File exists)
+openat(3, "ns", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 4
+
+# If /run/snapd/ns/<snap name>.mnt exists, enter that namespace:
+openat(3, "hello-world.mnt", O_RDONLY|O_CREAT|O_NOFOLLOW|O_CLOEXEC, 0600) = 5
+fstatfs(5, {f_type=0x6e736673, ...) = 0
+setns(5, CLONE_NEWNS) = 0
+... mount namespace setup finished, go on to setup the rest of the sandbox ...
+
+
+# Otherwise, create a new mount namespace
+unshare(CLONE_NEWNS)
+mount("none", "/", NULL, MS_REC|MS_SLAVE, NULL) = 0
+
+# Classic-only - mount rootfs in the namespace
+mkdir("/tmp/snap.rootfs_HkQghZ", 0700) = 0
+mount("/snap/ubuntu-core/current", "/tmp/snap.rootfs_HkQghZ", NULL, MS_BIND, NULL) = 0
+
+# Classic only - mount directories from host over rootfs
+mount("/dev", "/tmp/snap.rootfs_HkQghZ/dev", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/etc", "/tmp/snap.rootfs_HkQghZ/etc", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/home", "/tmp/snap.rootfs_HkQghZ/home", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/root", "/tmp/snap.rootfs_HkQghZ/root", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/proc", "/tmp/snap.rootfs_HkQghZ/proc", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/sys", "/tmp/snap.rootfs_HkQghZ/sys", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/tmp", "/tmp/snap.rootfs_HkQghZ/tmp", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/var/snap", "/tmp/snap.rootfs_HkQghZ/var/snap", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/var/lib/snapd", "/tmp/snap.rootfs_HkQghZ/var/lib/snapd", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/var/tmp", "/tmp/snap.rootfs_HkQghZ/var/tmp", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/run", "/tmp/snap.rootfs_HkQghZ/run", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/media", "/tmp/snap.rootfs_HkQghZ/media", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/lib/modules", "/tmp/snap.rootfs_HkQghZ/lib/modules", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/usr/src", "/tmp/snap.rootfs_HkQghZ/usr/src", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/var/log", "/tmp/snap.rootfs_HkQghZ/var/log", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/snap", "/tmp/snap.rootfs_HkQghZ/snap", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/snap/ubuntu-core/current/etc/alternatives", "/tmp/snap.rootfs_HkQghZ/etc/alternatives", NULL, MS_BIND|MS_SLAVE, NULL) = 0
+mount("/", "/tmp/snap.rootfs_HkQghZ/var/lib/snapd/hostfs", NULL, MS_RDONLY|MS_BIND, NULL) = 0
+
+# Classic only - pivot_root into the rootfs
+pivot_root(".", ".") = 0
+umount2(".", MNT_DETACH) = 0
+
+# Create a bind-mounted private /tmp
+mkdir("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1", 0700) = 0
+mkdir("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1/tmp", 01777) = 0
+mount("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1/tmp", "/tmp", NULL, MS_BIND, NULL) = 0
+mount("none", "/tmp", NULL, MS_PRIVATE, NULL) = 0
+
+# Create a per-snap /dev/pts
+mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, "newinstance,ptmxmode=0666,mode=0"...)
+mount("/dev/pts/ptmx", "/dev/ptmx", 0x5574dfe9a5c3, MS_BIND, NULL)
+
+# Classic only - process quirks mounts by:
+# creating temporary quirks directory for moving /var/lib/snapd aside
+mkdir("/tmp/snapd.quirks_xKIzG3", 0700) = 0
+# moving /var/lib/snapd aside
+mount("/var/lib/snapd", "/tmp/snapd.quirks_xKIzG3", NULL, MS_MOVE, NULL) = 0
+# creating a tmpfs on /var/lib for our mount points
+mount("none", "/var/lib", "tmpfs", MS_NOSUID|MS_NODEV, NULL) = 0
+# mimicking the vanilla /var/lib/* from the core snap in /var/lib in tmpfs
+# (the directories to mimic are dynamically determined and will vary as the
+# core snap changes. Syscalls for finding what to mount and creating the
+# mount points are omitted)
+mount("/snap/ubuntu-core/current/var/lib/apparmor", "/var/lib/apparmor", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND|MS_REC|MS_SLAVE, NULL) = 0
+mount("/snap/ubuntu-core/current/var/lib/classic", "/var/lib/classic", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/console-conf", "/var/lib/console-conf", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/dbus", "/var/lib/dbus", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/dhcp", "/var/lib/dhcp", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/extrausers", "/var/lib/extrausers", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/initramfs-tools", "/var/lib/initramfs-tools", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/initscripts", "/var/lib/initscripts", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/insserv", "/var/lib/insserv", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/logrotate", "/var/lib/logrotate", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/machines", "/var/lib/machines", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/misc", "/var/lib/misc", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/pam", "/var/lib/pam", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/python", "/var/lib/python", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/resolvconf", "/var/lib/resolvconf", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/snapd", "/var/lib/snapd", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/sudo", "/var/lib/sudo", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/systemd", "/var/lib/systemd", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/ubuntu-fan", "/var/lib/ubuntu-fan", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/ucf", "/var/lib/ucf", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/update-rc.d", "/var/lib/update-rc.d", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/urandom", "/var/lib/urandom", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/vim", "/var/lib/vim", ...) = 0
+mount("/snap/ubuntu-core/current/var/lib/waagent", "/var/lib/waagent", ...) = 0
+# unmounting the /var/lib/snapd that was just mimicked
+umount2("/var/lib/snapd", 0)
+# moving back the /var/lib/snapd that was set aside
+mount("/tmp/snapd.quirks_xKIzG3", "/var/lib/snapd", NULL, MS_MOVE, NULL) = 0
+# cleaning up the temporary directory
+rmdir("/tmp/snapd.quirks_xKIzG3") = 0
+# applying the actual quirk mounts as needed (for now, lxd, but more may
+# come). Eg:
+mount("/var/lib/snapd/hostfs/var/lib/lxd", "/var/lib/lxd", NULL, MS_REC|MS_SLAVE|MS_NODEV|MS_NOSUID|MS_NOEXEC) = 0
+# End quirk mounts on classic
+
+# Process snap-defined mounts (eg, for content interface, mount the source to
+# the target as defined in /var/lib/snapd/mount/snap.<name>.<command>.fstab)
+# Eg:
+mount("/snap/some-content-snap/current/src", "/snap/hello-world/current/dst", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND, NULL)
+
+# Bind mount this namespace to the application-specific NSFS magic file to
+# preserve it across snap invocations (an fchdir() happened just after the
+# unshare(), above).
+mount("/proc/12887/ns/mnt", "hello-world.mnt", NULL, MS_BIND, NULL) = 0
+... mount namespace setup finished, go on to setup the rest of the sandbox ...
--- /dev/null
+Nvidia on Arch
+==============
+
+On Arch nvidia support differs depending on the version of the driver user.
+Free drivers should work out of the box without any changes. Proprietary
+drivers were tested on the following driver versions:
+
+nvidia-340xx 340.96-13
+nvidia-340xx-libgl 340.96-1
+nvidia-340xx-utils 340.96-1
+
+The way the driver stack works was changed significantly in driver 364 and that
+version does not yet work correctly (we will gladly take patches if you beat us
+to the punch!). There is some ongoing work but it needs more investigation.
+
+Nvidia on Ubuntu
+================
+
+On Ubuntu nvidia drivers are provided in a different way and we believe that
+all versions work correctly.
+
+Nvidia on $DISTRO
+=================
+
+Free drivers should work everywhere. Support for proprietary drivers will be
+added on a case-by-case basis.
--- /dev/null
+To get all the syscalls, grab all the linux-libc-dev packages for all the
+architectures (eg, amd64, arm64, armhf, i386, powerpc, ppc64el) and put then
+in a directory. Then:
+
+mkdir extracted
+for i in ./*deb ; do
+ dpkg-deb -x $i ./extracted
+done
+
+for i in `find . -name "unistd*.h"|grep gnu` ; do egrep '^#define .*_NR_([a-z0-9_\-]*)' $i | awk '{print $2}' | sed 's/.*_NR_//' ; done|sort -u
+
+NOTE: syscall() isn't actually a syscall, it is a glibc wrapping to reference
+a syscall by number (therefore, it should be omitted from filter policy). ARM
+OABI did define this, but it has been obsoleted in EABI.
+
+For example, on Ubuntu 16.04 with the 4.4.0-16.32 Linux kernel, these are the
+syscalls:
+accept
+accept4
+access
+acct
+add_key
+adjtimex
+afs_syscall
+alarm
+arch_prctl
+arm_fadvise64_64
+arm_sync_file_range
+bdflush
+bind
+bpf
+break
+breakpoint
+brk
+cacheflush
+capget
+capset
+chdir
+chmod
+chown
+chown32
+chroot
+clock_adjtime
+clock_getres
+clock_gettime
+clock_nanosleep
+clock_settime
+clone
+close
+connect
+creat
+create_module
+delete_module
+dup
+dup2
+dup3
+epoll_create
+epoll_create1
+epoll_ctl
+epoll_ctl_old
+epoll_pwait
+epoll_wait
+epoll_wait_old
+eventfd
+eventfd2
+execve
+execveat
+exit
+exit_group
+faccessat
+fadvise64
+fadvise64_64
+fallocate
+fanotify_init
+fanotify_mark
+fchdir
+fchmod
+fchmodat
+fchown
+fchown32
+fchownat
+fcntl
+fcntl64
+fdatasync
+fgetxattr
+finit_module
+flistxattr
+flock
+fork
+fremovexattr
+fsetxattr
+fstat
+fstat64
+fstatat64
+fstatfs
+fstatfs64
+fsync
+ftime
+ftruncate
+ftruncate64
+futex
+futimesat
+getcpu
+getcwd
+getdents
+getdents64
+getegid
+getegid32
+geteuid
+geteuid32
+getgid
+getgid32
+getgroups
+getgroups32
+getitimer
+get_kernel_syms
+get_mempolicy
+getpeername
+getpgid
+getpgrp
+getpid
+getpmsg
+getppid
+getpriority
+getrandom
+getresgid
+getresgid32
+getresuid
+getresuid32
+getrlimit
+get_robust_list
+getrusage
+getsid
+getsockname
+getsockopt
+get_thread_area
+gettid
+gettimeofday
+getuid
+getuid32
+getxattr
+gtty
+idle
+init_module
+inotify_add_watch
+inotify_init
+inotify_init1
+inotify_rm_watch
+io_cancel
+ioctl
+io_destroy
+io_getevents
+ioperm
+iopl
+ioprio_get
+ioprio_set
+io_setup
+io_submit
+ipc
+kcmp
+kexec_file_load
+kexec_load
+keyctl
+kill
+lchown
+lchown32
+lgetxattr
+link
+linkat
+listen
+listxattr
+llistxattr
+_llseek
+lock
+lookup_dcookie
+lremovexattr
+lseek
+lsetxattr
+lstat
+lstat64
+madvise
+mbind
+membarrier
+memfd_create
+migrate_pages
+mincore
+mkdir
+mkdirat
+mknod
+mknodat
+mlock
+mlock2
+mlockall
+mmap
+mmap2
+modify_ldt
+mount
+move_pages
+mprotect
+mpx
+mq_getsetattr
+mq_notify
+mq_open
+mq_timedreceive
+mq_timedsend
+mq_unlink
+mremap
+msgctl
+msgget
+msgrcv
+msgsnd
+msync
+multiplexer
+munlock
+munlockall
+munmap
+name_to_handle_at
+nanosleep
+newfstatat
+_newselect
+nfsservctl
+nice
+oldfstat
+oldlstat
+oldolduname
+oldstat
+olduname
+open
+openat
+open_by_handle_at
+pause
+pciconfig_iobase
+pciconfig_read
+pciconfig_write
+perf_event_open
+personality
+pipe
+pipe2
+pivot_root
+poll
+ppoll
+prctl
+pread64
+preadv
+prlimit64
+process_vm_readv
+process_vm_writev
+prof
+profil
+pselect6
+ptrace
+putpmsg
+pwrite64
+pwritev
+query_module
+quotactl
+read
+readahead
+readdir
+readlink
+readlinkat
+readv
+reboot
+recv
+recvfrom
+recvmmsg
+recvmsg
+remap_file_pages
+removexattr
+rename
+renameat
+renameat2
+request_key
+restart_syscall
+rmdir
+rtas
+rt_sigaction
+rt_sigpending
+rt_sigprocmask
+rt_sigqueueinfo
+rt_sigreturn
+rt_sigsuspend
+rt_sigtimedwait
+rt_tgsigqueueinfo
+s390_pci_mmio_read
+s390_pci_mmio_write
+s390_runtime_instr
+sched_getaffinity
+sched_getattr
+sched_getparam
+sched_get_priority_max
+sched_get_priority_min
+sched_getscheduler
+sched_rr_get_interval
+sched_setaffinity
+sched_setattr
+sched_setparam
+sched_setscheduler
+sched_yield
+seccomp
+security
+select
+semctl
+semget
+semop
+semtimedop
+send
+sendfile
+sendfile64
+sendmmsg
+sendmsg
+sendto
+setdomainname
+setfsgid
+setfsgid32
+setfsuid
+setfsuid32
+setgid
+setgid32
+setgroups
+setgroups32
+sethostname
+setitimer
+set_mempolicy
+setns
+setpgid
+setpriority
+setregid
+setregid32
+setresgid
+setresgid32
+setresuid
+setresuid32
+setreuid
+setreuid32
+setrlimit
+set_robust_list
+setsid
+setsockopt
+set_thread_area
+set_tid_address
+settimeofday
+set_tls
+setuid
+setuid32
+setxattr
+sgetmask
+shmat
+shmctl
+shmdt
+shmget
+shutdown
+sigaction
+sigaltstack
+signal
+signalfd
+signalfd4
+sigpending
+sigprocmask
+sigreturn
+sigsuspend
+socket
+socketcall
+socketpair
+splice
+spu_create
+spu_run
+ssetmask
+stat
+stat64
+statfs
+statfs64
+stime
+stty
+subpage_prot
+swapcontext
+swapoff
+swapon
+switch_endian
+symlink
+symlinkat
+sync
+sync_file_range
+sync_file_range2
+syncfs
+syscall
+_sysctl
+sys_debug_setcontext
+sysfs
+sysinfo
+syslog
+tee
+tgkill
+time
+timer_create
+timer_delete
+timerfd
+timerfd_create
+timerfd_gettime
+timerfd_settime
+timer_getoverrun
+timer_gettime
+timer_settime
+times
+tkill
+truncate
+truncate64
+tuxcall
+ugetrlimit
+ulimit
+umask
+umount
+umount2
+uname
+unlink
+unlinkat
+unshare
+uselib
+userfaultfd
+usr26
+usr32
+ustat
+utime
+utimensat
+utimes
+vfork
+vhangup
+vm86
+vm86old
+vmsplice
+vserver
+wait4
+waitid
+waitpid
+write
+writev
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "apparmor-support.h"
+
+#include <string.h>
+#include <errno.h>
+#ifdef HAVE_APPARMOR
+#include <sys/apparmor.h>
+#endif // ifdef HAVE_APPARMOR
+
+#include "cleanup-funcs.h"
+#include "utils.h"
+
+// NOTE: Those constants map exactly what apparmor is returning and cannot be
+// changed without breaking apparmor functionality.
+#define SC_AA_ENFORCE_STR "enforce"
+#define SC_AA_COMPLAIN_STR "complain"
+#define SC_AA_MIXED_STR "mixed"
+#define SC_AA_UNCONFINED_STR "unconfined"
+
+void sc_init_apparmor_support(struct sc_apparmor *apparmor)
+{
+#ifdef HAVE_APPARMOR
+ // Use aa_is_enabled() to see if apparmor is available in the kernel and
+ // enabled at boot time. If it isn't log a diagnostic message and assume
+ // we're not confined.
+ if (aa_is_enabled() != true) {
+ switch (errno) {
+ case ENOSYS:
+ debug
+ ("apparmor extensions to the system are not available");
+ break;
+ case ECANCELED:
+ debug
+ ("apparmor is available on the system but has been disabled at boot");
+ break;
+ case ENOENT:
+ debug
+ ("apparmor is available but the interface but the interface is not available");
+ case EPERM:
+ // NOTE: fall-through
+ case EACCES:
+ debug
+ ("insufficient permissions to determine if apparmor is enabled");
+ break;
+ default:
+ debug("apparmor is not enabled: %s", strerror(errno));
+ break;
+ }
+ apparmor->is_confined = false;
+ apparmor->mode = SC_AA_NOT_APPLICABLE;
+ return;
+ }
+ // Use aa_getcon() to check the label of the current process and
+ // confinement type. Note that the returned label must be released with
+ // free() but the mode is a constant string that must not be freed.
+ char *label __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
+ char *mode = NULL;
+ if (aa_getcon(&label, &mode) < 0) {
+ die("cannot query current apparmor profile");
+ }
+ // The label has a special value "unconfined" that is applied to all
+ // processes without a dedicated profile. If that label is used then the
+ // current process is not confined. All other labels imply confinement.
+ if (label != NULL && strcmp(label, SC_AA_UNCONFINED_STR) == 0) {
+ apparmor->is_confined = false;
+ } else {
+ apparmor->is_confined = true;
+ }
+ // There are several possible results for the confinement type (mode) that
+ // are checked for below.
+ if (mode != NULL && strcmp(mode, SC_AA_COMPLAIN_STR) == 0) {
+ apparmor->mode = SC_AA_COMPLAIN;
+ } else if (mode != NULL && strcmp(mode, SC_AA_ENFORCE_STR) == 0) {
+ apparmor->mode = SC_AA_ENFORCE;
+ } else if (mode != NULL && strcmp(mode, SC_AA_MIXED_STR) == 0) {
+ apparmor->mode = SC_AA_MIXED;
+ } else {
+ apparmor->mode = SC_AA_INVALID;
+ }
+#else
+ apparmor->mode = SC_AA_NOT_APPLICABLE;
+ apparmor->is_confined = false;
+#endif // ifdef HAVE_APPARMOR
+}
+
+void
+sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile)
+{
+#ifdef HAVE_APPARMOR
+ debug("requesting changing of apparmor profile on next exec to %s",
+ profile);
+ if (aa_change_onexec(profile) < 0) {
+ if (secure_getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) {
+ die("cannot change profile for the next exec call");
+ }
+ }
+#endif // ifdef HAVE_APPARMOR
+}
+
+void
+sc_maybe_aa_change_hat(struct sc_apparmor *apparmor,
+ const char *subprofile, unsigned long magic_token)
+{
+#ifdef HAVE_APPARMOR
+ if (apparmor->is_confined) {
+ debug("changing apparmor hat to %s", subprofile);
+ if (aa_change_hat(subprofile, magic_token) < 0) {
+ die("cannot change apparmor hat");
+ }
+ }
+#endif // ifdef HAVE_APPARMOR
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_APPARMOR_SUPPORT_H
+#define SNAP_CONFINE_APPARMOR_SUPPORT_H
+
+#include <stdbool.h>
+
+/**
+ * Type of apparmor confinement.
+ **/
+enum sc_apparmor_mode {
+ // The enforcement mode was not recognized.
+ SC_AA_INVALID = -1,
+ // The enforcement mode is not applicable because apparmor is disabled.
+ SC_AA_NOT_APPLICABLE = 0,
+ // The enforcement mode is "enforcing"
+ SC_AA_ENFORCE = 1,
+ // The enforcement mode is "complain"
+ SC_AA_COMPLAIN,
+ // The enforcement mode is "mixed"
+ SC_AA_MIXED,
+};
+
+/**
+ * Data required to manage apparmor wrapper.
+ **/
+struct sc_apparmor {
+ // The mode of enforcement. In addition to the two apparmor defined modes
+ // can be also SC_AA_INVALID (unknown mode reported by apparmor) and
+ // SC_AA_NOT_APPLICABLE (when we're not linked with apparmor).
+ enum sc_apparmor_mode mode;
+ // Flag indicating that the current process is confined.
+ bool is_confined;
+};
+
+/**
+ * Initialize apparmor support.
+ *
+ * This operation should be done even when apparmor support is disabled at
+ * compile time. Internally the supplied structure is initialized based on the
+ * information returned from aa_getcon(2) or if apparmor is disabled at compile
+ * time, with built-in constants.
+ *
+ * The main action performed here is to check if snap-confine is currently
+ * confined, this information is used later in sc_maybe_change_apparmor_hat()
+ *
+ * As with many functions in the snap-confine tree, all errors result in
+ * process termination.
+ **/
+void sc_init_apparmor_support(struct sc_apparmor *apparmor);
+
+/**
+ * Maybe call aa_change_onexec(2)
+ *
+ * This function does nothing when apparmor support is not enabled at compile
+ * time. If apparmor is enabled then profile change request is attempted.
+ *
+ * As with many functions in the snap-confine tree, all errors result in
+ * process termination. As an exception, when SNAPPY_LAUNCHER_INSIDE_TESTS
+ * environment variable is set then the process is not terminated.
+ **/
+void
+sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile);
+
+/**
+ * Maybe call aa_change_hat(2)
+ *
+ * This function does nothing when apparmor support is not enabled at compile
+ * time. If apparmor is enabled then hat change is attempted.
+ *
+ * As with many functions in the snap-confine tree, all errors result in
+ * process termination.
+ **/
+void
+sc_maybe_aa_change_hat(struct sc_apparmor *apparmor,
+ const char *subprofile, unsigned long magic_token);
+
+#endif
--- /dev/null
+#include "config.h"
+#include "classic.h"
+
+#include <unistd.h>
+
+bool is_running_on_classic_distribution()
+{
+ // NOTE: keep this list sorted please
+ return false
+ || access("/var/lib/dpkg/status", F_OK) == 0
+ || access("/var/lib/pacman", F_OK) == 0
+ || access("/var/lib/portage", F_OK) == 0
+ || access("/var/lib/rpm", F_OK) == 0
+ || access("/sbin/procd", F_OK) == 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef SNAP_CONFINE_CLASSIC_H
+#define SNAP_CONFINE_CLASSIC_H
+
+#include <stdbool.h>
+
+// Location of the host filesystem directory in the core snap.
+#define SC_HOSTFS_DIR "/var/lib/snapd/hostfs"
+
+bool is_running_on_classic_distribution();
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "cleanup-funcs.h"
+#include "cleanup-funcs.c"
+
+#include <glib.h>
+
+// Test that cleanup functions are applied as expected
+static void test_cleanup_sanity()
+{
+ int called = 0;
+ void fn(int *ptr) {
+ called = 1;
+ }
+ {
+ int test __attribute__ ((cleanup(fn)));
+ test = 0;
+ test++;
+ }
+ g_assert_cmpint(called, ==, 1);
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/cleanup/sanity", test_cleanup_sanity);
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "cleanup-funcs.h"
+
+#include <mntent.h>
+#include <unistd.h>
+
+void sc_cleanup_string(char **ptr)
+{
+ free(*ptr);
+}
+
+void sc_cleanup_file(FILE ** ptr)
+{
+ if (*ptr != NULL)
+ fclose(*ptr);
+}
+
+void sc_cleanup_endmntent(FILE ** ptr)
+{
+ if (*ptr != NULL)
+ endmntent(*ptr);
+}
+
+#ifdef HAVE_SECCOMP
+void sc_cleanup_seccomp_release(scmp_filter_ctx * ptr)
+{
+ seccomp_release(*ptr);
+}
+#endif // HAVE_SECCOMP
+
+void sc_cleanup_closedir(DIR ** ptr)
+{
+ if (*ptr != NULL) {
+ closedir(*ptr);
+ }
+}
+
+void sc_cleanup_close(int *ptr)
+{
+ close(*ptr);
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_CLEANUP_FUNCS_H
+#define SNAP_CONFINE_CLEANUP_FUNCS_H
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif // HAVE_CONFIG_H
+
+#include <stdlib.h>
+#include <stdio.h>
+#ifdef HAVE_SECCOMP
+#include <seccomp.h>
+#endif // HAVE_SECCOMP
+#include <sys/types.h>
+#include <dirent.h>
+
+/**
+ * Free a dynamically allocated string.
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_string))).
+ **/
+void sc_cleanup_string(char **ptr);
+
+/**
+ * Close an open file.
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_file))).
+ **/
+void sc_cleanup_file(FILE ** ptr);
+
+/**
+ * Close an open file with endmntent(3)
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_endmntent))).
+ **/
+void sc_cleanup_endmntent(FILE ** ptr);
+
+#ifdef HAVE_SECCOMP
+/**
+ * Release a seccomp context with seccomp_release(3)
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_seccomp_release))).
+ **/
+void sc_cleanup_seccomp_release(scmp_filter_ctx * ptr);
+#endif // HAVE_SECCOMP
+
+/**
+ * Close an open directory with closedir(3)
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_closedir))).
+ **/
+void sc_cleanup_closedir(DIR ** ptr);
+
+/**
+ * Close an open file descriptor with close(2)
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_close))).
+ **/
+void sc_cleanup_close(int *ptr);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "mount-opt.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc != 2) {
+ printf("usage: decode-mount-opts OPT\n");
+ return 0;
+ }
+ char *end;
+ unsigned long mountflags = strtoul(argv[1], &end, 0);
+ if (*end != '\0') {
+ fprintf(stderr, "cannot parse given argument as a number\n");
+ return 1;
+ }
+ printf("%#lx is %s\n", mountflags, sc_mount_opt2str(mountflags));
+ return 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "error.h"
+#include "error.c"
+
+#include <errno.h>
+#include <glib.h>
+
+static void test_sc_error_init()
+{
+ struct sc_error *err;
+ // Create an error
+ err = sc_error_init("domain", 42, "printer is on %s", "fire");
+ g_assert_nonnull(err);
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+
+ // Inspect the exposed attributes
+ g_assert_cmpstr(sc_error_domain(err), ==, "domain");
+ g_assert_cmpint(sc_error_code(err), ==, 42);
+ g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire");
+}
+
+static void test_sc_error_init_from_errno()
+{
+ struct sc_error *err;
+ // Create an error
+ err = sc_error_init_from_errno(ENOENT, "printer is on %s", "fire");
+ g_assert_nonnull(err);
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+
+ // Inspect the exposed attributes
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_ERRNO_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, ENOENT);
+ g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire");
+}
+
+static void test_sc_error_cleanup()
+{
+ // Check that sc_error_cleanup() is safe to use.
+
+ // Cleanup is safe on NULL errors.
+ struct sc_error *err = NULL;
+ sc_cleanup_error(&err);
+
+ // Cleanup is safe on non-NULL errors.
+ err = sc_error_init("domain", 123, "msg");
+ g_assert_nonnull(err);
+ sc_cleanup_error(&err);
+ g_assert_null(err);
+}
+
+static void test_sc_error_domain__NULL()
+{
+ // Check that sc_error_domain() dies if called with NULL error.
+ if (g_test_subprocess()) {
+ // NOTE: the code below fools gcc 5.4 but your mileage may vary.
+ struct sc_error *err = NULL;
+ const char *domain = sc_error_domain(err);
+ (void)(domain);
+ g_test_message("expected not to reach this place");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr
+ ("cannot obtain error domain from NULL error\n");
+}
+
+static void test_sc_error_code__NULL()
+{
+ // Check that sc_error_code() dies if called with NULL error.
+ if (g_test_subprocess()) {
+ // NOTE: the code below fools gcc 5.4 but your mileage may vary.
+ struct sc_error *err = NULL;
+ int code = sc_error_code(err);
+ (void)(code);
+ g_test_message("expected not to reach this place");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("cannot obtain error code from NULL error\n");
+}
+
+static void test_sc_error_msg__NULL()
+{
+ // Check that sc_error_msg() dies if called with NULL error.
+ if (g_test_subprocess()) {
+ // NOTE: the code below fools gcc 5.4 but your mileage may vary.
+ struct sc_error *err = NULL;
+ const char *msg = sc_error_msg(err);
+ (void)(msg);
+ g_test_message("expected not to reach this place");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr
+ ("cannot obtain error message from NULL error\n");
+}
+
+static void test_sc_die_on_error__NULL()
+{
+ // Check that sc_die_on_error() does nothing if called with NULL error.
+ if (g_test_subprocess()) {
+ sc_die_on_error(NULL);
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_passed();
+}
+
+static void test_sc_die_on_error__regular()
+{
+ // Check that sc_die_on_error() dies if called with an error.
+ if (g_test_subprocess()) {
+ struct sc_error *err =
+ sc_error_init("domain", 42, "just testing");
+ sc_die_on_error(err);
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("just testing\n");
+}
+
+static void test_sc_die_on_error__errno()
+{
+ // Check that sc_die_on_error() dies if called with an errno-based error.
+ if (g_test_subprocess()) {
+ struct sc_error *err =
+ sc_error_init_from_errno(ENOENT, "just testing");
+ sc_die_on_error(err);
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("just testing: No such file or directory\n");
+}
+
+static void test_sc_error_forward__nothing()
+{
+ // Check that forwarding NULL does exactly that.
+ struct sc_error *recipient = (void *)0xDEADBEEF;
+ struct sc_error *err = NULL;
+ sc_error_forward(&recipient, err);
+ g_assert_null(recipient);
+}
+
+static void test_sc_error_forward__something_somewhere()
+{
+ // Check that forwarding a real error works OK.
+ struct sc_error *recipient = NULL;
+ struct sc_error *err = sc_error_init("domain", 42, "just testing");
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+ g_assert_nonnull(err);
+ sc_error_forward(&recipient, err);
+ g_assert_nonnull(recipient);
+}
+
+static void test_sc_error_forward__something_nowhere()
+{
+ // Check that forwarding a real error nowhere calls die()
+ if (g_test_subprocess()) {
+ // NOTE: the code below fools gcc 5.4 but your mileage may vary.
+ struct sc_error **err_ptr = NULL;
+ struct sc_error *err =
+ sc_error_init("domain", 42, "just testing");
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+ g_assert_nonnull(err);
+ sc_error_forward(err_ptr, err);
+ g_test_message("expected not to reach this place");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("just testing\n");
+}
+
+static void test_sc_error_match__typical()
+{
+ // NULL error doesn't match anything.
+ g_assert_false(sc_error_match(NULL, "domain", 42));
+
+ // Non-NULL error matches if domain and code both match.
+ struct sc_error *err = sc_error_init("domain", 42, "just testing");
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+ g_assert_true(sc_error_match(err, "domain", 42));
+ g_assert_false(sc_error_match(err, "domain", 1));
+ g_assert_false(sc_error_match(err, "other-domain", 42));
+ g_assert_false(sc_error_match(err, "other-domain", 1));
+}
+
+static void test_sc_error_match__NULL_domain()
+{
+ // Using a NULL domain is a fatal bug.
+ if (g_test_subprocess()) {
+ // NOTE: the code below fools gcc 5.4 but your mileage may vary.
+ struct sc_error *err = NULL;
+ const char *domain = NULL;
+ g_assert_false(sc_error_match(err, domain, 42));
+ g_test_message("expected not to reach this place");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("cannot match error to a NULL domain\n");
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/error/sc_error_init", test_sc_error_init);
+ g_test_add_func("/error/sc_error_init_from_errno",
+ test_sc_error_init_from_errno);
+ g_test_add_func("/error/sc_error_cleanup", test_sc_error_cleanup);
+ g_test_add_func("/error/sc_error_domain/NULL",
+ test_sc_error_domain__NULL);
+ g_test_add_func("/error/sc_error_code/NULL", test_sc_error_code__NULL);
+ g_test_add_func("/error/sc_error_msg/NULL", test_sc_error_msg__NULL);
+ g_test_add_func("/error/sc_die_on_error/NULL",
+ test_sc_die_on_error__NULL);
+ g_test_add_func("/error/sc_die_on_error/regular",
+ test_sc_die_on_error__regular);
+ g_test_add_func("/error/sc_die_on_error/errno",
+ test_sc_die_on_error__errno);
+ g_test_add_func("/error/sc_error_formward/nothing",
+ test_sc_error_forward__nothing);
+ g_test_add_func("/error/sc_error_formward/something_somewhere",
+ test_sc_error_forward__something_somewhere);
+ g_test_add_func("/error/sc_error_formward/something_nowhere",
+ test_sc_error_forward__something_nowhere);
+ g_test_add_func("/error/sc_error_match/typical",
+ test_sc_error_match__typical);
+ g_test_add_func("/error/sc_error_match/NULL_domain",
+ test_sc_error_match__NULL_domain);
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "error.h"
+
+// To get vasprintf
+#define _GNU_SOURCE
+
+#include "utils.h"
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+struct sc_error {
+ // Error domain defines a scope for particular error codes.
+ const char *domain;
+ // Code differentiates particular errors for the programmer.
+ // The code may be zero if the particular meaning is not relevant.
+ int code;
+ // Message carries a formatted description of the problem.
+ char *msg;
+};
+
+static struct sc_error *sc_error_initv(const char *domain, int code,
+ const char *msgfmt, va_list ap)
+{
+ struct sc_error *err = calloc(1, sizeof *err);
+ if (err == NULL) {
+ die("cannot allocate memory for error object");
+ }
+ err->domain = domain;
+ err->code = code;
+ if (vasprintf(&err->msg, msgfmt, ap) == -1) {
+ die("cannot format error message");
+ }
+ return err;
+}
+
+struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt,
+ ...)
+{
+ va_list ap;
+ va_start(ap, msgfmt);
+ struct sc_error *err = sc_error_initv(domain, code, msgfmt, ap);
+ va_end(ap);
+ return err;
+}
+
+struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt,
+ ...)
+{
+ va_list ap;
+ va_start(ap, msgfmt);
+ struct sc_error *err =
+ sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap);
+ va_end(ap);
+ return err;
+}
+
+const char *sc_error_domain(struct sc_error *err)
+{
+ if (err == NULL) {
+ die("cannot obtain error domain from NULL error");
+ }
+ return err->domain;
+}
+
+int sc_error_code(struct sc_error *err)
+{
+ if (err == NULL) {
+ die("cannot obtain error code from NULL error");
+ }
+ return err->code;
+}
+
+const char *sc_error_msg(struct sc_error *err)
+{
+ if (err == NULL) {
+ die("cannot obtain error message from NULL error");
+ }
+ return err->msg;
+}
+
+void sc_error_free(struct sc_error *err)
+{
+ if (err != NULL) {
+ free(err->msg);
+ err->msg = NULL;
+ free(err);
+ }
+}
+
+void sc_cleanup_error(struct sc_error **ptr)
+{
+ sc_error_free(*ptr);
+ *ptr = NULL;
+}
+
+void sc_die_on_error(struct sc_error *error)
+{
+ if (error != NULL) {
+ if (strcmp(sc_error_domain(error), SC_ERRNO_DOMAIN) == 0) {
+ // Set errno just before the call to die() as it is used internally
+ errno = sc_error_code(error);
+ die("%s", sc_error_msg(error));
+ } else {
+ errno = 0;
+ die("%s", sc_error_msg(error));
+ }
+ }
+}
+
+void sc_error_forward(struct sc_error **recipient, struct sc_error *error)
+{
+ if (recipient != NULL) {
+ *recipient = error;
+ } else {
+ sc_die_on_error(error);
+ }
+}
+
+bool sc_error_match(struct sc_error *error, const char *domain, int code)
+{
+ if (domain == NULL) {
+ die("cannot match error to a NULL domain");
+ }
+ if (error == NULL) {
+ return false;
+ }
+ return strcmp(sc_error_domain(error), domain) == 0
+ && sc_error_code(error) == code;
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_ERROR_H
+#define SNAP_CONFINE_ERROR_H
+
+#include <stdbool.h>
+
+#define SC_GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)
+
+/**
+ * The attribute returns_nonnull is only supported by GCC versions >= 4.9.0.
+ * Enable building of snap-confine on platforms that are stuck with older
+ * GCC versions.
+ **/
+#if SC_GCC_VERSION >= 40900
+#define SC_APPEND_RETURNS_NONNULL , returns_nonnull
+#else
+#define SC_APPEND_RETURNS_NONNULL
+#endif
+
+/**
+ * This module defines APIs for simple error management.
+ *
+ * Errors are allocated objects that can be returned and passed around from
+ * functions. Errors carry a formatted message and optionally a scoped error
+ * code. The code is coped with a string "domain" that simply acts as a
+ * namespace for various interacting modules.
+ **/
+
+/**
+ * Opaque error structure.
+ **/
+struct sc_error;
+
+/**
+ * Error domain for errors related to system errno.
+ **/
+#define SC_ERRNO_DOMAIN "errno"
+
+/**
+ * Initialize a new error object.
+ *
+ * The domain is a cookie-like string that allows the caller to distinguish
+ * between "namespaces" of error codes. It should be a static string that is
+ * provided by the caller. Both the domain and the error code can be retrieved
+ * later.
+ *
+ * This function calls die() in case of memory allocation failure.
+ **/
+__attribute__ ((warn_unused_result,
+ format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL))
+struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt,
+ ...);
+
+/**
+ * Initialize an errno-based error.
+ *
+ * The error carries a copy of errno and a custom error message as designed by
+ * the caller. See sc_error_init() for a more complete description.
+ *
+ * This function calls die() in case of memory allocation failure.
+ **/
+__attribute__ ((warn_unused_result,
+ format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL))
+struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt,
+ ...);
+
+/**
+ * Get the error domain out of an error object.
+ *
+ * The error domain acts as a namespace for error codes.
+ * No change of ownership takes place.
+ **/
+__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL))
+const char *sc_error_domain(struct sc_error *err);
+
+/**
+ * Get the error code out of an error object.
+ *
+ * The error code is scoped by the error domain.
+ *
+ * An error code of zero is special-cased to indicate that no particular error
+ * code is reserved for this error and it's not something that the programmer
+ * can rely on programmatically. This can be used to return an error message
+ * without having to allocate a distinct code for each one.
+ **/
+__attribute__ ((warn_unused_result))
+int sc_error_code(struct sc_error *err);
+
+/**
+ * Get the error message out of an error object.
+ *
+ * The error message is bound to the life-cycle of the error object.
+ * No change of ownership takes place.
+ **/
+__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL))
+const char *sc_error_msg(struct sc_error *err);
+
+/**
+ * Free an error object.
+ *
+ * The error object can be NULL.
+ **/
+void sc_error_free(struct sc_error *error);
+
+/**
+ * Cleanup an error with sc_error_free()
+ *
+ * This function is designed to be used with
+ * __attribute__((cleanup(sc_cleanup_error))).
+ **/
+__attribute__ ((nonnull))
+void sc_cleanup_error(struct sc_error **ptr);
+
+/**
+ *
+ * Die if there's an error.
+ *
+ * This function is a correct way to die() if the passed error is not NULL.
+ *
+ * The error message is derived from the data in the error, using the special
+ * errno domain to provide additional information if that is available.
+ **/
+void sc_die_on_error(struct sc_error *error);
+
+/**
+ * Forward an error to the caller.
+ *
+ * This tries to forward an error to the caller. If this is impossible because
+ * the caller did not provide a location for the error to be stored then the
+ * sc_die_on_error() is called as a safety measure.
+ *
+ * Change of ownership takes place and the error is now stored in the recipient.
+ **/
+// NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With
+// the attribute in place GCC optimizes some things out and tests fail.
+void sc_error_forward(struct sc_error **recipient, struct sc_error *error);
+
+/**
+ * Check if a given error matches the specified domain and code.
+ *
+ * It is okay to match a NULL error, the function simply returns false in that
+ * case. The domain cannot be NULL though.
+ **/
+__attribute__ ((warn_unused_result))
+bool sc_error_match(struct sc_error *error, const char *domain, int code);
+
+#endif
--- /dev/null
+dist_man_MANS = snap-confine.5 snap-discard-ns.5 ubuntu-core-launcher.1
+
+CLEANFILES = snap-confine.5 snap-discard-ns.5 ubuntu-core-launcher.1
+EXTRA_DIST = snap-confine.rst snap-discard-ns.rst ubuntu-core-launcher.rst
+
+%.5: %.rst
+ rst2man $^ > $@
+
+ubuntu-core-launcher.1: ubuntu-core-launcher.rst
+ rst2man $^ > $@
--- /dev/null
+==============
+ snap-confine
+==============
+
+-----------------------------------------------
+internal tool for confining snappy applications
+-----------------------------------------------
+
+:Author: zygmunt.krynicki@canonical.com
+:Date: 2016-10-05
+:Copyright: Canonical Ltd.
+:Version: 1.0.43
+:Manual section: 5
+:Manual group: snappy
+
+SYNOPSIS
+========
+
+ snap-confine SECURITY_TAG COMMAND [...ARGUMENTS]
+
+DESCRIPTION
+===========
+
+The `snap-confine` is a program used internally by `snapd` to construct a
+confined execution environment for snap applications.
+
+OPTIONS
+=======
+
+The `snap-confine` program does not support any options.
+
+FEATURES
+========
+
+Apparmor profiles
+-----------------
+
+`snap-confine` switches to the apparmor profile `$SECURITY_TAG`. The profile is
+**mandatory** and `snap-confine` will refuse to run without it.
+
+has to be loaded into the kernel prior to using `snap-confine`. Typically this
+is arranged for by `snapd`. The profile contains rich description of what the
+application process is allowed to do, this includes system calls, file paths,
+access patterns, linux capabilities, etc. The apparmor profile can also do
+extensive dbus mediation. Refer to apparmor documentation for more details.
+
+Seccomp profiles
+----------------
+
+`snap-confine` looks for the `/var/lib/snapd/seccomp/profiles/$SECURITY_TAG`
+file. This file is **mandatory** and `snap-confine` will refuse to run without
+it.
+
+The file is read and parsed using a custom syntax that describes the set of
+allowed system calls and optionally their arguments. The profile is then used
+to confine the started application.
+
+As a security precaution disallowed system calls cause the started application
+executable to be killed by the kernel. In the future this restriction may be
+lifted to return `EPERM` instead.
+
+Mount profiles
+--------------
+
+`snap-confine` looks for the `/var/lib/snapd/mount/$SECURITY_TAG.fstab` file.
+If present it is read, parsed and treated like a typical `fstab(5)` file.
+The mount directives listed there are executed in order. All directives must
+succeed as any failure will abort execution.
+
+By default all mount entries start with the following flags: `bind`, `ro`,
+`nodev`, `nosuid`. Some of those flags can be reversed by an appropriate
+option (e.g. `rw` can cause the mount point to be writable).
+
+As a security precaution only `bind` mounts are supported at this time.
+
+Quirks
+------
+
+`snap-confine` contains a quirk system that emulates some or the behavior of
+the older versions of snap-confine that certain snaps (still in devmode but
+useful and important) have grown to rely on. This section documents the list of
+quirks:
+
+- The /var/lib/lxd directory, if it exists on the host, is made available in
+ the execution environment. This allows various snaps, while running in
+ devmode, to access the LXD socket. LP: #1613845
+
+Sharing of the mount namespace
+------------------------------
+
+As of version 1.0.41 all the applications from the same snap will share the
+same mount namespace. Applications from different snaps continue to use
+separate mount namespaces.
+
+ENVIRONMENT
+===========
+
+`snap-confine` responds to the following environment variables
+
+`SNAP_CONFINE_DEBUG`:
+ When defined the program will print additional diagnostic information about
+ the actions being performed. All the output goes to stderr.
+
+The following variables are only used when `snap-confine` is not setuid root.
+This is only applicable when testing the program itself.
+
+`SNAPPY_LAUNCHER_INSIDE_TESTS`:
+ Internal variable that should not be relied upon.
+
+`SNAP_CONFINE_NO_ROOT`:
+ Internal variable that should not be relied upon.
+
+`SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR`:
+ Internal variable that should not be relied upon.
+
+`SNAP_USER_DATA`:
+ Full path to the directory like /home/$LOGNAME/snap/$SNAP_NAME/$SNAP_REVISION.
+
+ This directory is created by snap-confine on startup. This is a temporary
+ feature that will be merged into snapd's snap-run command. The set of directories
+ that can be created is confined with apparmor.
+
+FILES
+=====
+
+`snap-confine` uses the following files:
+
+`/var/lib/snapd/mount/*.fstab`:
+
+ Description of the mount profile.
+
+`/var/lib/snapd/seccomp/profiles/*`:
+
+ Description of the seccomp profile.
+
+`/run/snapd/ns/`:
+
+ Directory used to keep shared mount namespaces.
+
+ `snap-confine` internally converts this directory to a private bind mount.
+ Semantically the behavior is identical to the following mount commands:
+
+ mount --bind /run/snapd/ns /run/snapd/ns
+ mount --make-private /run/snapd/ns
+
+`/run/snapd/ns/.lock`:
+
+ A `flock(2)`-based lock file acquired to create and convert
+ `/run/snapd/ns/` to a private bind mount.
+
+`/run/snapd/ns/$SNAP_NAME.lock`:
+
+ A `flock(2)`-based lock file acquired to create or join the mount namespace
+ represented as `/run/snaps/ns/$SNAP_NAME.mnt`.
+
+`/run/snapd/ns/$SNAP_NAME.mnt`:
+
+ This file can be either:
+
+ - An empty file that may be seen before the mount namespace is preserved or
+ when the mount namespace is unmounted.
+ - A file belonging to the `nsfs` file system, representing a fully
+ populated mount namespace of a given snap. The file is bind mounted from
+ `/proc/self/ns/mnt` from the first process in any snap.
+
+`/proc/self/mountinfo`:
+
+ This file is read to decide if `/run/snapd/ns/` needs to be created and
+ converted to a private bind mount, as described above.
+
+Note that the apparmor profile is external to `snap-confine` and is loaded
+directly into the kernel. The actual apparmor profile is managed by `snapd`.
+
+BUGS
+====
+
+Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug
--- /dev/null
+================
+ snap-discard-ns
+================
+
+------------------------------------------------------------------------
+internal tool for discarding preserved namespaces of snappy applications
+------------------------------------------------------------------------
+
+:Author: zygmunt.krynicki@canonical.com
+:Date: 2016-10-05
+:Copyright: Canonical Ltd.
+:Version: 1.0.43
+:Manual section: 5
+:Manual group: snappy
+
+SYNOPSIS
+========
+
+ snap-discard-ns SNAP_NAME
+
+DESCRIPTION
+===========
+
+The `snap-discard-ns` is a program used internally by `snapd` to discard a preserved
+mount namespace of a particular snap.
+
+OPTIONS
+=======
+
+The `snap-discard-ns` program does not support any options.
+
+ENVIRONMENT
+===========
+
+`snap-discard-ns` responds to the following environment variables
+
+`SNAP_CONFINE_DEBUG`:
+ When defined the program will print additional diagnostic information about
+ the actions being performed. All the output goes to stderr.
+
+FILES
+=====
+
+`snap-discard-ns` uses the following files:
+
+`/run/snapd/ns/$SNAP_NAME.mnt`:
+
+ The preserved mount namespace that is unmounted by `snap-discard-ns`.
+
+BUGS
+====
+
+Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug
--- /dev/null
+======================
+ ubuntu-core-launcher
+======================
+
+-----------------------------------------------
+internal tool for confining snappy applications
+-----------------------------------------------
+
+:Author: zygmunt.krynicki@canonical.com
+:Date: 2016-10-05
+:Copyright: Canonical Ltd.
+:Version: 1.0.43
+:Manual section: 1
+:Manual group: snappy
+
+SYNOPSIS
+========
+
+ ubuntu-core-launcher SECURITY_TAG SECURITY_TAG COMMAND [...ARGUMENTS]
+
+DESCRIPTION
+===========
+
+This program is a thin wrapper around `snap-confine`. Please do not rely on it,
+it is likely to be removed in the next release.
+
+OPTIONS
+=======
+
+The `ubuntu-core-launcher` program does not support any options.
+
+BUGS
+====
+
+Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug
+
+SEE ALSO
+========
+
+`snap-confine(5)`
--- /dev/null
+From 1ef45eb31cacd58c4c62e1fd26aa63a1f3d031a7 Mon Sep 17 00:00:00 2001
+From: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
+Date: Thu, 29 Sep 2016 15:11:15 +0200
+Subject: [PATCH] Add printk-based debugging to pivot_root
+
+This patch changes pivot_root to make it obvious which error exit path
+was taken. It might be useful to apply to debug and investigate how
+undocumented requirements of pivot_root are not met.
+
+Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
+---
+ fs/namespace.c | 70 ++++++++++++++++++++++++++++++++++++++++++++--------------
+ 1 file changed, 53 insertions(+), 17 deletions(-)
+
+diff --git a/fs/namespace.c b/fs/namespace.c
+index 877fc2c..6e15d1d 100644
+--- a/fs/namespace.c
++++ b/fs/namespace.c
+@@ -2993,57 +2993,93 @@ SYSCALL_DEFINE2(pivot_root, const char __user *, new_root,
+ return -EPERM;
+
+ error = user_path_dir(new_root, &new);
+- if (error)
++ if (error) {
++ printk(KERN_ERR "user_path_dir(new_root, &new) returned an error\n");
+ goto out0;
++ }
+
+ error = user_path_dir(put_old, &old);
+- if (error)
++ if (error) {
++ printk(KERN_ERR "user_path_dir(put_old, &old) returned an error\n");
+ goto out1;
++ }
+
+ error = security_sb_pivotroot(&old, &new);
+- if (error)
++ if (error) {
++ printk(KERN_ERR "security_sb_pivotroot(&old, &new) returned an error\n");
+ goto out2;
++ }
+
+ get_fs_root(current->fs, &root);
+ old_mp = lock_mount(&old);
+ error = PTR_ERR(old_mp);
+- if (IS_ERR(old_mp))
++ if (IS_ERR(old_mp)) {
++ printk(KERN_ERR "IS_ERR(old_mp)\n");
+ goto out3;
++ }
+
+ error = -EINVAL;
+ new_mnt = real_mount(new.mnt);
+ root_mnt = real_mount(root.mnt);
+ old_mnt = real_mount(old.mnt);
+- if (IS_MNT_SHARED(old_mnt) ||
+- IS_MNT_SHARED(new_mnt->mnt_parent) ||
+- IS_MNT_SHARED(root_mnt->mnt_parent))
++ if (IS_MNT_SHARED(old_mnt)) {
++ printk(KERN_ERR "IS_MNT_SHARED(old_mnt)\n");
++ goto out4;
++ }
++ if (IS_MNT_SHARED(new_mnt->mnt_parent)) {
++ printk(KERN_ERR "IS_MNT_SHARED(new_mnt->mnt_parent)\n");
+ goto out4;
+- if (!check_mnt(root_mnt) || !check_mnt(new_mnt))
++ }
++ if (IS_MNT_SHARED(root_mnt->mnt_parent)) {
++ printk(KERN_ERR "IS_MNT_SHARED(root_mnt->mnt_parent)\n");
+ goto out4;
+- if (new_mnt->mnt.mnt_flags & MNT_LOCKED)
++ }
++ if (!check_mnt(root_mnt) || !check_mnt(new_mnt)) {
++ printk(KERN_ERR "!check_mnt(root_mnt) || !check_mnt(new_mnt)\n");
++ goto out4;
++ }
++ if (new_mnt->mnt.mnt_flags & MNT_LOCKED) {
++ printk(KERN_ERR "new_mnt->mnt.mnt_flags & MNT_LOCKED\n");
+ goto out4;
++ }
+ error = -ENOENT;
+- if (d_unlinked(new.dentry))
++ if (d_unlinked(new.dentry)) {
++ printk(KERN_ERR "d_unlinked(new.dentry)\n");
+ goto out4;
++ }
+ error = -EBUSY;
+- if (new_mnt == root_mnt || old_mnt == root_mnt)
++ if (new_mnt == root_mnt || old_mnt == root_mnt) {
++ printk(KERN_ERR "new_mnt == root_mnt || old_mnt == root_mnt\n");
+ goto out4; /* loop, on the same file system */
++ }
+ error = -EINVAL;
+- if (root.mnt->mnt_root != root.dentry)
++ if (root.mnt->mnt_root != root.dentry) {
++ printk(KERN_ERR "root.mnt->mnt_root != root.dentry\n");
+ goto out4; /* not a mountpoint */
+- if (!mnt_has_parent(root_mnt))
++ }
++ if (!mnt_has_parent(root_mnt)) {
++ printk(KERN_ERR "!mnt_has_parent(root_mnt)\n");
+ goto out4; /* not attached */
++ }
+ root_mp = root_mnt->mnt_mp;
+- if (new.mnt->mnt_root != new.dentry)
++ if (new.mnt->mnt_root != new.dentry) {
++ printk(KERN_ERR "new.mnt->mnt_root != new.dentry\n");
+ goto out4; /* not a mountpoint */
+- if (!mnt_has_parent(new_mnt))
++ }
++ if (!mnt_has_parent(new_mnt)) {
++ printk(KERN_ERR "!mnt_has_parent(new_mnt)\n");
+ goto out4; /* not attached */
++ }
+ /* make sure we can reach put_old from new_root */
+- if (!is_path_reachable(old_mnt, old.dentry, &new))
++ if (!is_path_reachable(old_mnt, old.dentry, &new)) {
++ printk(KERN_ERR "!is_path_reachable(old_mnt, old.dentry, &new)\n");
+ goto out4;
++ }
+ /* make certain new is below the root */
+- if (!is_path_reachable(new_mnt, new.dentry, &root))
++ if (!is_path_reachable(new_mnt, new.dentry, &root)) {
++ printk(KERN_ERR "!is_path_reachable(new_mnt, new.dentry, &root)\n");
+ goto out4;
++ }
+ root_mp->m_count++; /* pin it so it won't go away */
+ lock_mount_hash();
+ detach_mnt(new_mnt, &parent_path);
+--
+2.7.4
+
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mount-opt.h"
+#include "mount-opt.c"
+
+#include <sys/mount.h>
+#include <glib.h>
+
+static void test_sc_mount_opt2str()
+{
+ g_assert_cmpstr(sc_mount_opt2str(0), ==, "");
+ g_assert_cmpstr(sc_mount_opt2str(MS_RDONLY), ==, "ro");
+ g_assert_cmpstr(sc_mount_opt2str(MS_NOSUID), ==, "nosuid");
+ g_assert_cmpstr(sc_mount_opt2str(MS_NODEV), ==, "nodev");
+ g_assert_cmpstr(sc_mount_opt2str(MS_NOEXEC), ==, "noexec");
+ g_assert_cmpstr(sc_mount_opt2str(MS_SYNCHRONOUS), ==, "sync");
+ g_assert_cmpstr(sc_mount_opt2str(MS_REMOUNT), ==, "remount");
+ g_assert_cmpstr(sc_mount_opt2str(MS_MANDLOCK), ==, "mand");
+ g_assert_cmpstr(sc_mount_opt2str(MS_DIRSYNC), ==, "dirsync");
+ g_assert_cmpstr(sc_mount_opt2str(MS_NOATIME), ==, "noatime");
+ g_assert_cmpstr(sc_mount_opt2str(MS_NODIRATIME), ==, "nodiratime");
+ g_assert_cmpstr(sc_mount_opt2str(MS_BIND), ==, "bind");
+ g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_BIND), ==, "rbind");
+ g_assert_cmpstr(sc_mount_opt2str(MS_MOVE), ==, "move");
+ g_assert_cmpstr(sc_mount_opt2str(MS_SILENT), ==, "silent");
+ g_assert_cmpstr(sc_mount_opt2str(MS_POSIXACL), ==, "acl");
+ g_assert_cmpstr(sc_mount_opt2str(MS_UNBINDABLE), ==, "unbindable");
+ g_assert_cmpstr(sc_mount_opt2str(MS_PRIVATE), ==, "private");
+ g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_PRIVATE), ==, "rprivate");
+ g_assert_cmpstr(sc_mount_opt2str(MS_SLAVE), ==, "slave");
+ g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_SLAVE), ==, "rslave");
+ g_assert_cmpstr(sc_mount_opt2str(MS_SHARED), ==, "shared");
+ g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_SHARED), ==, "rshared");
+ g_assert_cmpstr(sc_mount_opt2str(MS_RELATIME), ==, "relatime");
+ g_assert_cmpstr(sc_mount_opt2str(MS_KERNMOUNT), ==, "kernmount");
+ g_assert_cmpstr(sc_mount_opt2str(MS_I_VERSION), ==, "iversion");
+ g_assert_cmpstr(sc_mount_opt2str(MS_STRICTATIME), ==, "strictatime");
+ g_assert_cmpstr(sc_mount_opt2str(MS_LAZYTIME), ==, "lazytime");
+ // MS_NOSEC is not defined in userspace
+ // MS_BORN is not defined in userspace
+ g_assert_cmpstr(sc_mount_opt2str(MS_ACTIVE), ==, "active");
+ g_assert_cmpstr(sc_mount_opt2str(MS_NOUSER), ==, "nouser");
+ g_assert_cmpstr(sc_mount_opt2str(0x300), ==, "0x300");
+ // random compositions do work
+ g_assert_cmpstr(sc_mount_opt2str(MS_RDONLY | MS_NOEXEC | MS_BIND), ==,
+ "ro,noexec,bind");
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str);
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mount-opt.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+
+const char *sc_mount_opt2str(unsigned long flags)
+{
+ static char buf[1000];
+ unsigned long used = 0;
+ strcpy(buf, "");
+#define F(FLAG, TEXT) do if (flags & (FLAG)) { strcat(buf, #TEXT ","); flags ^= (FLAG); } while (0)
+ F(MS_RDONLY, ro);
+ F(MS_NOSUID, nosuid);
+ F(MS_NODEV, nodev);
+ F(MS_NOEXEC, noexec);
+ F(MS_SYNCHRONOUS, sync);
+ F(MS_REMOUNT, remount);
+ F(MS_MANDLOCK, mand);
+ F(MS_DIRSYNC, dirsync);
+ F(MS_NOATIME, noatime);
+ F(MS_NODIRATIME, nodiratime);
+ if (flags & MS_BIND) {
+ if (flags & MS_REC) {
+ strcat(buf, "rbind,");
+ used |= MS_REC;
+ } else {
+ strcat(buf, "bind,");
+ }
+ flags ^= MS_BIND;
+ }
+ F(MS_MOVE, move);
+ // The MS_REC flag handled separately by affected flags (MS_BIND,
+ // MS_PRIVATE, MS_SLAVE, MS_SHARED)
+ // XXX: kernel has MS_VERBOSE, glibc has MS_SILENT, both use the same constant
+ F(MS_SILENT, silent);
+ F(MS_POSIXACL, acl);
+ F(MS_UNBINDABLE, unbindable);
+ if (flags & MS_PRIVATE) {
+ if (flags & MS_REC) {
+ strcat(buf, "rprivate,");
+ used |= MS_REC;
+ } else {
+ strcat(buf, "private,");
+ }
+ flags ^= MS_PRIVATE;
+ }
+ if (flags & MS_SLAVE) {
+ if (flags & MS_REC) {
+ strcat(buf, "rslave,");
+ used |= MS_REC;
+ } else {
+ strcat(buf, "slave,");
+ }
+ flags ^= MS_SLAVE;
+ }
+ if (flags & MS_SHARED) {
+ if (flags & MS_REC) {
+ strcat(buf, "rshared,");
+ used |= MS_REC;
+ } else {
+ strcat(buf, "shared,");
+ }
+ flags ^= MS_SHARED;
+ }
+ flags ^= used; // this is just for MS_REC
+ F(MS_RELATIME, relatime);
+ F(MS_KERNMOUNT, kernmount);
+ F(MS_I_VERSION, iversion);
+ F(MS_STRICTATIME, strictatime);
+#ifndef MS_LAZYTIME
+#define MS_LAZYTIME (1<<25)
+#endif
+ F(MS_LAZYTIME, lazytime);
+#ifndef MS_NOSEC
+#define MS_NOSEC (1 << 28)
+#endif
+ F(MS_NOSEC, nosec);
+#ifndef MS_BORN
+#define MS_BORN (1 << 29)
+#endif
+ F(MS_BORN, born);
+ F(MS_ACTIVE, active);
+ F(MS_NOUSER, nouser);
+#undef F
+ // Render any flags that are unaccounted for.
+ if (flags) {
+ char of[128];
+ sprintf(of, "%#lx", flags);
+ strcat(buf, of);
+ }
+ // Chop the excess comma from the end.
+ size_t len = strlen(buf);
+ if (len > 0 && buf[len - 1] == ',') {
+ buf[len - 1] = 0;
+ }
+ return buf;
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_MOUNT_OPT_H
+#define SNAP_CONFINE_MOUNT_OPT_H
+
+/**
+ * Convert flags for mount(2) system call to a string representation.
+ *
+ * The function uses an internal static buffer that is overwritten on each
+ * request.
+ **/
+const char *sc_mount_opt2str(unsigned long flags);
+
+#endif // SNAP_CONFINE_MOUNT_OPT_H
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+#include "mount-support-nvidia.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "classic.h"
+#include "cleanup-funcs.h"
+#include "utils.h"
+
+#ifdef NVIDIA_ARCH
+
+// List of globs that describe nvidia userspace libraries.
+// This list was compiled from the following packages.
+//
+// https://www.archlinux.org/packages/extra/x86_64/nvidia-304xx-libgl/files/
+// https://www.archlinux.org/packages/extra/x86_64/nvidia-304xx-utils/files/
+// https://www.archlinux.org/packages/extra/x86_64/nvidia-340xx-libgl/files/
+// https://www.archlinux.org/packages/extra/x86_64/nvidia-340xx-utils/files/
+// https://www.archlinux.org/packages/extra/x86_64/nvidia-libgl/files/
+// https://www.archlinux.org/packages/extra/x86_64/nvidia-utils/files/
+//
+// FIXME: this doesn't yet work with libGLX and libglvnd redirector
+// FIXME: this still doesn't work with the 361 driver
+static const char *nvidia_globs[] = {
+ "/usr/lib/libEGL.so*",
+ "/usr/lib/libEGL_nvidia.so*",
+ "/usr/lib/libGL.so*",
+ "/usr/lib/libOpenGL.so*",
+ "/usr/lib/libGLESv1_CM.so*",
+ "/usr/lib/libGLESv1_CM_nvidia.so*",
+ "/usr/lib/libGLESv2.so*",
+ "/usr/lib/libGLESv2_nvidia.so*",
+ "/usr/lib/libGLX_indirect.so*",
+ "/usr/lib/libGLX_nvidia.so*",
+ "/usr/lib/libGLX.so*",
+ "/usr/lib/libGLdispatch.so*",
+ "/usr/lib/libGLU.so*",
+ "/usr/lib/libXvMCNVIDIA.so*",
+ "/usr/lib/libXvMCNVIDIA_dynamic.so*",
+ "/usr/lib/libcuda.so*",
+ "/usr/lib/libnvcuvid.so*",
+ "/usr/lib/libnvidia-cfg.so*",
+ "/usr/lib/libnvidia-compiler.so*",
+ "/usr/lib/libnvidia-eglcore.so*",
+ "/usr/lib/libnvidia-encode.so*",
+ "/usr/lib/libnvidia-fatbinaryloader.so*",
+ "/usr/lib/libnvidia-fbc.so*",
+ "/usr/lib/libnvidia-glcore.so*",
+ "/usr/lib/libnvidia-glsi.so*",
+ "/usr/lib/libnvidia-ifr.so*",
+ "/usr/lib/libnvidia-ml.so*",
+ "/usr/lib/libnvidia-ptxjitcompiler.so*",
+ "/usr/lib/libnvidia-tls.so*",
+};
+
+static const size_t nvidia_globs_len =
+ sizeof nvidia_globs / sizeof *nvidia_globs;
+
+// Populate libgl_dir with a symlink farm to files matching glob_list.
+//
+// The symbolic links are made in one of two ways. If the library found is a
+// file a regular symlink "$libname" -> "/path/to/hostfs/$libname" is created.
+// If the library is a symbolic link then relative links are kept as-is but
+// absolute links are translated to have "/path/to/hostfs" up front so that
+// they work after the pivot_root elsewhere.
+static void sc_populate_libgl_with_hostfs_symlinks(const char *libgl_dir,
+ const char *glob_list[],
+ size_t glob_list_len)
+{
+ glob_t glob_res __attribute__ ((__cleanup__(globfree))) = {
+ .gl_pathv = NULL};
+ // Find all the entries matching the list of globs
+ for (size_t i = 0; i < glob_list_len; ++i) {
+ const char *glob_pattern = glob_list[i];
+ int err =
+ glob(glob_pattern, i ? GLOB_APPEND : 0, NULL, &glob_res);
+ // Not all of the files have to be there (they differ depending on the
+ // driver version used). Ignore all errors that are not GLOB_NOMATCH.
+ if (err != 0 && err != GLOB_NOMATCH) {
+ die("cannot search using glob pattern %s: %d",
+ glob_pattern, err);
+ }
+ }
+ // Symlink each file found
+ for (size_t i = 0; i < glob_res.gl_pathc; ++i) {
+ char symlink_name[512];
+ char symlink_target[512];
+ const char *pathname = glob_res.gl_pathv[i];
+ char *pathname_copy
+ __attribute__ ((cleanup(sc_cleanup_string))) =
+ strdup(pathname);
+ char *filename = basename(pathname_copy);
+ struct stat stat_buf;
+ int err = lstat(pathname, &stat_buf);
+ if (err != 0) {
+ die("cannot stat file %s", pathname);
+ }
+ switch (stat_buf.st_mode & S_IFMT) {
+ case S_IFLNK:;
+ // Read the target of the symbolic link
+ char hostfs_symlink_target[512];
+ ssize_t num_read;
+ hostfs_symlink_target[0] = 0;
+ num_read =
+ readlink(pathname, hostfs_symlink_target,
+ sizeof hostfs_symlink_target);
+ if (num_read == -1) {
+ die("cannot read symbolic link %s", pathname);
+ }
+ hostfs_symlink_target[num_read] = 0;
+ if (hostfs_symlink_target[0] == '/') {
+ must_snprintf(symlink_target,
+ sizeof symlink_target,
+ "/var/lib/snapd/hostfs%s",
+ hostfs_symlink_target);
+ } else {
+ // Keep relative symlinks as-is, so that they point to -> libfoo.so.0.123
+ must_snprintf(symlink_target,
+ sizeof symlink_target, "%s",
+ hostfs_symlink_target);
+ }
+ break;
+ case S_IFREG:
+ must_snprintf(symlink_target,
+ sizeof symlink_target,
+ "/var/lib/snapd/hostfs%s", pathname);
+ break;
+ default:
+ debug("ignoring unsupported entry: %s", pathname);
+ continue;
+ }
+ must_snprintf(symlink_name, sizeof symlink_name,
+ "%s/%s", libgl_dir, filename);
+ debug("creating symbolic link %s -> %s", symlink_name,
+ symlink_target);
+ if (symlink(symlink_target, symlink_name) != 0) {
+ die("cannot create symbolic link %s -> %s",
+ symlink_name, symlink_target);
+ }
+ }
+}
+
+static void sc_mount_nvidia_driver_arch(const char *rootfs_dir)
+{
+ // Bind mount a tmpfs on $rootfs_dir/var/lib/snapd/lib/gl
+ char buf[512];
+ must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir,
+ "/var/lib/snapd/lib/gl");
+ const char *libgl_dir = buf;
+ debug("mounting tmpfs at %s", libgl_dir);
+ if (mount("none", libgl_dir, "tmpfs", MS_NODEV | MS_NOEXEC, NULL) != 0) {
+ die("cannot mount tmpfs at %s", libgl_dir);
+ };
+ // Populate libgl_dir with symlinks to libraries from hostfs
+ sc_populate_libgl_with_hostfs_symlinks(libgl_dir, nvidia_globs,
+ nvidia_globs_len);
+ // Remount .../lib/gl read only
+ debug("remounting tmpfs as read-only %s", libgl_dir);
+ if (mount(NULL, buf, NULL, MS_REMOUNT | MS_RDONLY, NULL) != 0) {
+ die("cannot remount %s as read-only", buf);
+ }
+}
+
+#endif // ifdef NVIDIA_ARCH
+
+#ifdef NVIDIA_UBUNTU
+
+struct sc_nvidia_driver {
+ int major_version;
+ int minor_version;
+};
+
+#define SC_NVIDIA_DRIVER_VERSION_FILE "/sys/module/nvidia/version"
+#define SC_LIBGL_DIR "/var/lib/snapd/lib/gl"
+
+static void sc_probe_nvidia_driver(struct sc_nvidia_driver *driver)
+{
+ FILE *file __attribute__ ((cleanup(sc_cleanup_file))) = NULL;
+ debug("opening file describing nvidia driver version");
+ file = fopen(SC_NVIDIA_DRIVER_VERSION_FILE, "rt");
+ if (file == NULL) {
+ if (errno == ENOENT) {
+ debug("nvidia driver version file doesn't exist");
+ driver->major_version = 0;
+ driver->minor_version = 0;
+ return;
+ }
+ die("cannot open file describing nvidia driver version");
+ }
+ // Driver version format is MAJOR.MINOR where both MAJOR and MINOR are
+ // integers. We can use sscanf to parse this data.
+ if (fscanf
+ (file, "%d.%d", &driver->major_version,
+ &driver->minor_version) != 2) {
+ die("cannot parse nvidia driver version string");
+ }
+ debug("parsed nvidia driver version: %d.%d", driver->major_version,
+ driver->minor_version);
+}
+
+static void sc_mount_nvidia_driver_ubuntu(const char *rootfs_dir)
+{
+ struct sc_nvidia_driver driver;
+ sc_probe_nvidia_driver(&driver);
+ if (driver.major_version != 0) {
+ // Bind mount the binary nvidia driver into /var/lib/snapd/lib/gl.
+ char src[PATH_MAX], dst[PATH_MAX];
+ must_snprintf(src, sizeof src, "/usr/lib/nvidia-%d",
+ driver.major_version);
+ must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir,
+ SC_LIBGL_DIR);
+ debug("bind mounting nvidia driver %s -> %s", src, dst);
+ if (mount(src, dst, NULL, MS_BIND, NULL) != 0) {
+ die("cannot bind mount nvidia driver %s -> %s", src,
+ dst);
+ }
+ }
+}
+#endif // ifdef NVIDIA_UBUNTU
+
+void sc_mount_nvidia_driver(const char *rootfs_dir)
+{
+#ifdef NVIDIA_UBUNTU
+ sc_mount_nvidia_driver_ubuntu(rootfs_dir);
+#endif // ifdef NVIDIA_UBUNTU
+#ifdef NVIDIA_ARCH
+ sc_mount_nvidia_driver_arch(rootfs_dir);
+#endif // ifdef NVIDIA_ARCH
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_MOUNT_SUPPORT_NVIDIA_H
+#define SNAP_CONFINE_MOUNT_SUPPORT_NVIDIA_H
+
+/**
+ * Make the Nvidia driver from the classic distribution available in the snap
+ * execution environment.
+ *
+ * This function may be a no-op, depending on build-time configuration options.
+ * If enabled the behavior differs from one distribution to another because of
+ * differences in classic packaging and perhaps version of the Nvidia driver.
+ * This function is designed to be called before pivot_root() switched the root
+ * filesystem.
+ *
+ * On Ubuntu, there are several versions of the binary Nvidia driver. The
+ * drivers are all installed in /usr/lib/nvidia-$MAJOR_VERSION where
+ * MAJOR_VERSION is an integer like 304, 331, 340, 346, 352 or 361. The driver
+ * is located by inspecting /sys/modules/nvidia/version which contains the
+ * string "$MAJOR_VERSION.$MINOR_VERSION". The appropriate directory is then
+ * bind mounted to /var/lib/snapd/lib/gl relative relative to the location of
+ * the root filesystem directory provided as an argument.
+ *
+ * On Arch another approach is used. Because the actual driver installs a
+ * number of shared objects into /usr/lib, they cannot be bind mounted
+ * directly. Instead a tmpfs is mounted on /var/lib/snapd/lib/gl. The tmpfs is
+ * subsequently populated with symlinks that point to a number of files in the
+ * /usr/lib directory on the classic filesystem. After the pivot_root() call
+ * those symlinks rely on the /var/lib/snapd/hostfs directory as a "gateway".
+ **/
+void sc_mount_nvidia_driver(const char *rootfs_dir);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mount-support.h"
+#include "mount-support.c"
+#include "mount-support-nvidia.h"
+#include "mount-support-nvidia.c"
+
+#include <glib.h>
+
+static void replace_slashes_with_NUL(char *path, size_t len)
+{
+ for (size_t i = 0; i < len; i++) {
+ if (path[i] == '/')
+ path[i] = '\0';
+ }
+}
+
+static void test_get_nextpath__typical()
+{
+ char path[] = "/some/path";
+ size_t offset = 0;
+ size_t fulllen = strlen(path);
+
+ // Prepare path for usage with get_nextpath() by replacing
+ // all path separators with the NUL byte.
+ replace_slashes_with_NUL(path, fulllen);
+
+ // Run get_nextpath a few times to see what happens.
+ char *result;
+ result = get_nextpath(path, &offset, fulllen);
+ g_assert_cmpstr(result, ==, "some");
+ result = get_nextpath(path, &offset, fulllen);
+ g_assert_cmpstr(result, ==, "path");
+ result = get_nextpath(path, &offset, fulllen);
+ g_assert_cmpstr(result, ==, NULL);
+}
+
+static void test_get_nextpath__weird()
+{
+ char path[] = "..///path";
+ size_t offset = 0;
+ size_t fulllen = strlen(path);
+
+ // Prepare path for usage with get_nextpath() by replacing
+ // all path separators with the NUL byte.
+ replace_slashes_with_NUL(path, fulllen);
+
+ // Run get_nextpath a few times to see what happens.
+ char *result;
+ result = get_nextpath(path, &offset, fulllen);
+ g_assert_cmpstr(result, ==, "path");
+ result = get_nextpath(path, &offset, fulllen);
+ g_assert_cmpstr(result, ==, NULL);
+}
+
+static void test_is_subdir()
+{
+ // Sensible exaples are sensible
+ g_assert_true(is_subdir("/dir/subdir", "/dir/"));
+ g_assert_true(is_subdir("/dir/subdir", "/dir"));
+ g_assert_true(is_subdir("/dir/", "/dir"));
+ g_assert_true(is_subdir("/dir", "/dir"));
+ // Also without leading slash
+ g_assert_true(is_subdir("dir/subdir", "dir/"));
+ g_assert_true(is_subdir("dir/subdir", "dir"));
+ g_assert_true(is_subdir("dir/", "dir"));
+ g_assert_true(is_subdir("dir", "dir"));
+ // Some more ideas
+ g_assert_true(is_subdir("//", "/"));
+ g_assert_true(is_subdir("/", "/"));
+ g_assert_true(is_subdir("", ""));
+ // but this is not true
+ g_assert_false(is_subdir("/", "/dir"));
+ g_assert_false(is_subdir("/rid", "/dir"));
+ g_assert_false(is_subdir("/different/dir", "/dir"));
+ g_assert_false(is_subdir("/", ""));
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/mount/get_nextpath/typical",
+ test_get_nextpath__typical);
+ g_test_add_func("/mount/get_nextpath/weird", test_get_nextpath__weird);
+ g_test_add_func("/mount/is_subdir", test_is_subdir);
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "config.h"
+#include "mount-support.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <mntent.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "classic.h"
+#include "cleanup-funcs.h"
+#include "mount-support-nvidia.h"
+#include "quirks.h"
+#include "snap.h"
+#include "utils.h"
+
+#define MAX_BUF 1000
+
+/*!
+ * The void directory.
+ *
+ * Snap confine moves to that directory in case it cannot retain the current
+ * working directory across the pivot_root call.
+ **/
+#define SC_VOID_DIR "/var/lib/snapd/void"
+
+/**
+ * Get the path to the mounted core snap on the host distribution.
+ *
+ * The core snap may be named just "core" (preferred) or "ubuntu-core"
+ * (legacy). The mount point dependes on build-time configuration and may
+ * differ from distribution to distribution.
+ **/
+static const char *sc_get_outer_core_mount_point()
+{
+ const char *core_path = SNAP_MOUNT_DIR "/core/current/";
+ const char *ubuntu_core_path = SNAP_MOUNT_DIR "/ubuntu-core/current/";
+ static const char *result = NULL;
+ if (result == NULL) {
+ if (access(core_path, F_OK) == 0) {
+ // Use the "core" snap if available.
+ result = core_path;
+ } else if (access(ubuntu_core_path, F_OK) == 0) {
+ // If not try to fall back to the "ubuntu-core" snap.
+ result = ubuntu_core_path;
+ } else {
+ die("cannot locate the core snap");
+ }
+ }
+ return result;
+}
+
+// TODO: simplify this, after all it is just a tmpfs
+// TODO: fold this into bootstrap
+static void setup_private_mount(const char *security_tag)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char tmpdir[MAX_BUF] = { 0 };
+
+ // Create a 0700 base directory, this is the base dir that is
+ // protected from other users.
+ //
+ // Under that basedir, we put a 1777 /tmp dir that is then bind
+ // mounted for the applications to use
+ must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid,
+ security_tag);
+ if (mkdtemp(tmpdir) == NULL) {
+ die("cannot create temporary directory essential for private /tmp");
+ }
+ // now we create a 1777 /tmp inside our private dir
+ mode_t old_mask = umask(0);
+ char *d = strdup(tmpdir);
+ if (!d) {
+ die("cannot allocate memory for string copy");
+ }
+ must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d);
+ free(d);
+
+ if (mkdir(tmpdir, 01777) != 0) {
+ die("cannot create temporary directory for private /tmp");
+ }
+ umask(old_mask);
+
+ // chdir to '/' since the mount won't apply to the current directory
+ char *pwd = get_current_dir_name();
+ if (pwd == NULL)
+ die("cannot get current working directory");
+ if (chdir("/") != 0)
+ die("cannot change directory to '/'");
+
+ // MS_BIND is there from linux 2.4
+ if (mount(tmpdir, "/tmp", NULL, MS_BIND, NULL) != 0) {
+ die("cannot bind mount private /tmp");
+ }
+ // MS_PRIVATE needs linux > 2.6.11
+ if (mount("none", "/tmp", NULL, MS_PRIVATE, NULL) != 0) {
+ die("cannot change sharing on /tmp to make it private");
+ }
+ // do the chown after the bind mount to avoid potential shenanigans
+ if (chown("/tmp/", uid, gid) < 0) {
+ die("cannot change ownership of /tmp");
+ }
+ // chdir to original directory
+ if (chdir(pwd) != 0)
+ die("cannot change current working directory to the original directory");
+ free(pwd);
+
+ // ensure we set the various TMPDIRs to our newly created tmpdir
+ const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL };
+ int i;
+ for (i = 0; tmpd[i] != NULL; i++) {
+ if (setenv(tmpd[i], "/tmp", 1) != 0) {
+ die("cannot set environment variable '%s'", tmpd[i]);
+ }
+ }
+}
+
+// TODO: fold this into bootstrap
+static void setup_private_pts()
+{
+ // See https://www.kernel.org/doc/Documentation/filesystems/devpts.txt
+ //
+ // Ubuntu by default uses devpts 'single-instance' mode where
+ // /dev/pts/ptmx is mounted with ptmxmode=0000. We don't want to change
+ // the startup scripts though, so we follow the instructions in point
+ // '4' of 'User-space changes' in the above doc. In other words, after
+ // unshare(CLONE_NEWNS), we mount devpts with -o
+ // newinstance,ptmxmode=0666 and then bind mount /dev/pts/ptmx onto
+ // /dev/ptmx
+
+ struct stat st;
+
+ // Make sure /dev/pts/ptmx exists, otherwise we are in legacy mode
+ // which doesn't provide the isolation we require.
+ if (stat("/dev/pts/ptmx", &st) != 0) {
+ die("cannot stat /dev/pts/ptmx");
+ }
+ // Make sure /dev/ptmx exists so we can bind mount over it
+ if (stat("/dev/ptmx", &st) != 0) {
+ die("cannot stat /dev/ptmx");
+ }
+ // Since multi-instance, use ptmxmode=0666. The other options are
+ // copied from /etc/default/devpts
+ if (mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL,
+ "newinstance,ptmxmode=0666,mode=0620,gid=5")) {
+ die("cannot mount a new instance of /dev/pts");
+ }
+
+ if (mount("/dev/pts/ptmx", "/dev/ptmx", "none", MS_BIND, 0)) {
+ die("cannot mount /dev/pts/ptmx at /dev/ptmx'");
+ }
+}
+
+/*
+ * Setup mount profiles as described by snapd.
+ *
+ * This function reads /var/lib/snapd/mount/$security_tag.fstab as a fstab(5) file
+ * and executes the mount requests described there.
+ *
+ * Currently only bind mounts are allowed. All bind mounts are read only by
+ * default though the `rw` flag can be used.
+ *
+ * This function is called with the rootfs being "consistent" so that it is
+ * either the core snap on an all-snap system or the core snap + punched holes
+ * on a classic system.
+ **/
+static void sc_setup_mount_profiles(const char *security_tag)
+{
+ debug("%s: %s", __FUNCTION__, security_tag);
+
+ FILE *f __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL;
+ const char *mount_profile_dir = "/var/lib/snapd/mount";
+
+ char profile_path[PATH_MAX];
+ must_snprintf(profile_path, sizeof(profile_path), "%s/%s.fstab",
+ mount_profile_dir, security_tag);
+
+ debug("opening mount profile %s", profile_path);
+ f = setmntent(profile_path, "r");
+ // it is ok for the file to not exist
+ if (f == NULL && errno == ENOENT) {
+ debug("mount profile %s doesn't exist, ignoring", profile_path);
+ return;
+ }
+ // however any other error is a real error
+ if (f == NULL) {
+ die("cannot open %s", profile_path);
+ }
+
+ struct mntent *m = NULL;
+ while ((m = getmntent(f)) != NULL) {
+ debug("read mount entry\n"
+ "\tmnt_fsname: %s\n"
+ "\tmnt_dir: %s\n"
+ "\tmnt_type: %s\n"
+ "\tmnt_opts: %s\n"
+ "\tmnt_freq: %d\n"
+ "\tmnt_passno: %d",
+ m->mnt_fsname, m->mnt_dir, m->mnt_type,
+ m->mnt_opts, m->mnt_freq, m->mnt_passno);
+ int flags = MS_BIND | MS_RDONLY | MS_NODEV | MS_NOSUID;
+ debug("initial flags are: bind,ro,nodev,nosuid");
+ if (strcmp(m->mnt_type, "none") != 0) {
+ die("cannot honor mount profile, only 'none' filesystem type is supported");
+ }
+ if (hasmntopt(m, "bind") == NULL) {
+ die("cannot honor mount profile, the bind mount flag is mandatory");
+ }
+ if (hasmntopt(m, "rw") != NULL) {
+ flags &= ~MS_RDONLY;
+ }
+ if (mount(m->mnt_fsname, m->mnt_dir, NULL, flags, NULL) != 0) {
+ die("cannot mount %s at %s with options %s",
+ m->mnt_fsname, m->mnt_dir, m->mnt_opts);
+ }
+ }
+}
+
+struct sc_mount {
+ const char *path;
+ bool is_bidirectional;
+};
+
+struct sc_mount_config {
+ const char *rootfs_dir;
+ // The struct is terminated with an entry with NULL path.
+ const struct sc_mount *mounts;
+ bool on_classic;
+};
+
+/**
+ * Bootstrap mount namespace.
+ *
+ * This is a chunk of tricky code that lets us have full control over the
+ * layout and direction of propagation of mount events. The documentation below
+ * assumes knowledge of the 'sharedsubtree.txt' document from the kernel source
+ * tree.
+ *
+ * As a reminder two definitions are quoted below:
+ *
+ * A 'propagation event' is defined as event generated on a vfsmount
+ * that leads to mount or unmount actions in other vfsmounts.
+ *
+ * A 'peer group' is defined as a group of vfsmounts that propagate
+ * events to each other.
+ *
+ * (end of quote).
+ *
+ * The main idea is to setup a mount namespace that has a root filesystem with
+ * vfsmounts and peer groups that, depending on the location, either isolate
+ * or share with the rest of the system.
+ *
+ * The vast majority of the filesystem is shared in one direction. Events from
+ * the outside (from the main mount namespace) propagate inside (to namespaces
+ * of particular snaps) so things like new snap revisions, mounted drives, etc,
+ * just show up as expected but even if a snap is exploited or malicious in
+ * nature it cannot affect anything in another namespace where it might cause
+ * security or stability issues.
+ *
+ * Selected directories (today just /media) can be shared in both directions.
+ * This allows snaps with sufficient privileges to either create, through the
+ * mount system call, additional mount points that are visible by the rest of
+ * the system (both the main mount namespace and namespaces of individual
+ * snaps) or remove them, through the unmount system call.
+ **/
+static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config)
+{
+ char scratch_dir[] = "/tmp/snap.rootfs_XXXXXX";
+ char src[PATH_MAX];
+ char dst[PATH_MAX];
+ if (mkdtemp(scratch_dir) == NULL) {
+ die("cannot create temporary directory for the root file system");
+ }
+ // NOTE: at this stage we just called unshare(CLONE_NEWNS). We are in a new
+ // mount namespace and have a private list of mounts.
+ debug("scratch directory for constructing namespace: %s", scratch_dir);
+ // Make the root filesystem recursively shared. This way propagation events
+ // will be shared with main mount namespace.
+ debug("performing operation: mount --make-rshared /");
+ if (mount("none", "/", NULL, MS_REC | MS_SHARED, NULL) < 0) {
+ die("cannot perform operation: mount --make-rshared /");
+ }
+ // Bind mount the temporary scratch directory for root filesystem over
+ // itself so that it is a mount point. This is done so that it can become
+ // unbindable as explained below.
+ debug("performing operation: mount --bind %s %s", scratch_dir,
+ scratch_dir);
+ if (mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL) < 0) {
+ die("cannot perform operation: mount --bind %s %s", scratch_dir,
+ scratch_dir);
+ }
+ // Make the scratch directory unbindable.
+ //
+ // This is necessary as otherwise a mount loop can occur and the kernel
+ // would crash. The term unbindable simply states that it cannot be bind
+ // mounted anywhere. When we construct recursive bind mounts below this
+ // guarantees that this directory will not be replicated anywhere.
+ debug("performing operation: mount --make-unbindable %s", scratch_dir);
+ if (mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL) < 0) {
+ die("cannot perform operation: mount --make-unbindable %s",
+ scratch_dir);
+ }
+ // Recursively bind mount desired root filesystem directory over the
+ // scratch directory. This puts the initial content into the scratch space
+ // and serves as a foundation for all subsequent operations below.
+ //
+ // The mount is recursive because it can either be applied to the root
+ // filesystem of a core system (aka all-snap) or the core snap on a classic
+ // system. In the former case we need recursive bind mounts to accurately
+ // replicate the state of the root filesystem into the scratch directory.
+ debug("performing operation: mount --rbind %s %s", config->rootfs_dir,
+ scratch_dir);
+ if (mount(config->rootfs_dir, scratch_dir, NULL, MS_REC | MS_BIND, NULL)
+ < 0) {
+ die("cannot perform operation: mount --rbind %s %s",
+ config->rootfs_dir, scratch_dir);
+ }
+ // Make the scratch directory recursively private. Nothing done there will
+ // be shared with any peer group, This effectively detaches us from the
+ // original namespace and coupled with pivot_root below serves as the
+ // foundation of the mount sandbox.
+ debug("performing operation: mount --make-rslave %s", scratch_dir);
+ if (mount("none", scratch_dir, NULL, MS_REC | MS_SLAVE, NULL) < 0) {
+ die("cannot perform operation: mount --make-rslave %s",
+ scratch_dir);
+ }
+ // Bind mount certain directories from the host filesystem to the scratch
+ // directory. By default mount events will propagate in both into and out
+ // of the peer group. This way the running application can alter any global
+ // state visible on the host and in other snaps. This can be restricted by
+ // disabling the "is_bidirectional" flag as can be seen below.
+ for (const struct sc_mount * mnt = config->mounts; mnt->path != NULL;
+ mnt++) {
+ if (mnt->is_bidirectional && mkdir(mnt->path, 0755) < 0 &&
+ errno != EEXIST) {
+ die("cannot create %s", mnt->path);
+ }
+ must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, mnt->path);
+ debug("performing operation: mount --rbind %s %s", mnt->path,
+ dst);
+ if (mount(mnt->path, dst, NULL, MS_REC | MS_BIND, NULL) < 0) {
+ die("cannot perform operation: mount --rbind %s %s",
+ mnt->path, dst);
+ }
+ if (!mnt->is_bidirectional) {
+ // Mount events will only propagate inwards to the namespace. This
+ // way the running application cannot alter any global state apart
+ // from that of its own snap.
+ debug("performing operation: mount --make-rslave %s",
+ dst);
+ if (mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL) !=
+ 0) {
+ die("cannot perform operation: mount --make-rslave %s", dst);
+ }
+ }
+ }
+ // Since we mounted /etc from the host filesystem to the scratch directory,
+ // we may need to put /etc/alternatives from the desired root filesystem
+ // (e.g. the core snap) back. This way the behavior of running snaps is not
+ // affected by the alternatives directory from the host, if one exists.
+ //
+ // https://bugs.launchpad.net/snap-confine/+bug/1580018
+ const char *etc_alternatives = "/etc/alternatives";
+ if (access(etc_alternatives, F_OK) == 0) {
+ must_snprintf(src, sizeof src, "%s%s", config->rootfs_dir,
+ etc_alternatives);
+ must_snprintf(dst, sizeof dst, "%s%s", scratch_dir,
+ etc_alternatives);
+ debug("performing operation: mount --bind %s %s", src, dst);
+ if (mount(src, dst, NULL, MS_BIND, NULL) != 0) {
+ die("cannot perform operation: mount --bind %s %s", src,
+ dst);
+ }
+ debug("performing operation: mount --make-slave %s", dst);
+ if (mount("none", dst, NULL, MS_SLAVE, NULL) != 0) {
+ die("cannot perform operation: mount --make-slave %s",
+ dst);
+ }
+ }
+ // Bind mount the directory where all snaps are mounted. The location of
+ // the this directory on the host filesystem may not match the location in
+ // the desired root filesystem. In the "core" and "ubuntu-core" snaps the
+ // directory is always /snap. On the host it is a build-time configuration
+ // option stored in SNAP_MOUNT_DIR.
+ must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir);
+ debug("performing operation: mount --rbind %s %s", SNAP_MOUNT_DIR, dst);
+ if (mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC | MS_SLAVE, NULL)
+ < 0) {
+ die("cannot perform operation: mount --rbind -o slave %s %s",
+ SNAP_MOUNT_DIR, dst);
+ }
+ debug("performing operation: mount --make-rslave %s", dst);
+ if (mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL) < 0) {
+ die("cannot perform operation: mount --make-rslave %s", dst);
+ }
+ // Create the hostfs directory if one is missing. This directory is a part
+ // of packaging now so perhaps this code can be removed later.
+ if (access(SC_HOSTFS_DIR, F_OK) != 0) {
+ debug("creating missing hostfs directory");
+ if (mkdir(SC_HOSTFS_DIR, 0755) != 0) {
+ die("cannot perform operation: mkdir %s",
+ SC_HOSTFS_DIR);
+ }
+ }
+ // Make the upcoming "put_old" directory for pivot_root private so that
+ // mount events don't propagate to any peer group. In practice pivot root
+ // has a number of undocumented requirements and one of them is that the
+ // "put_old" directory (the second argument) cannot be shared in any way.
+ must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR);
+ debug("performing operation: mount --bind %s %s", dst, dst);
+ if (mount(dst, dst, NULL, MS_BIND, NULL) < 0) {
+ die("cannot perform operation: mount --bind %s %s", dst, dst);
+ }
+ debug("performing operation: mount --make-private %s", dst);
+ if (mount("none", dst, NULL, MS_PRIVATE, NULL) < 0) {
+ die("cannot perform operation: mount --make-private %s", dst);
+ }
+ // On classic mount the nvidia driver. Ideally this would be done in an
+ // uniform way after pivot_root but this is good enough and requires less
+ // code changes the nvidia code assumes it has access to the existing
+ // pre-pivot filesystem.
+ if (config->on_classic) {
+ sc_mount_nvidia_driver(scratch_dir);
+ }
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // pivot_root
+ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ // Use pivot_root to "chroot" into the scratch directory.
+ //
+ // Q: Why are we using something as esoteric as pivot_root(2)?
+ // A: Because this makes apparmor handling easy. Using a normal chroot
+ // makes all apparmor rules conditional. We are either running on an
+ // all-snap system where this would-be chroot didn't happen and all the
+ // rules see / as the root file system _OR_ we are running on top of a
+ // classic distribution and this chroot has now moved all paths to
+ // /tmp/snap.rootfs_*.
+ //
+ // Because we are using unshare(2) with CLONE_NEWNS we can essentially use
+ // pivot_root just like chroot but this makes apparmor unaware of the old
+ // root so everything works okay.
+ //
+ // HINT: If you are debugging this and are trying to see why pivot_root
+ // happens to return EINVAL with any changes you may be making, please
+ // consider applying
+ // misc/0001-Add-printk-based-debugging-to-pivot_root.patch to your tree
+ // kernel.
+ debug("performing operation: pivot_root %s %s", scratch_dir, dst);
+ if (syscall(SYS_pivot_root, scratch_dir, dst) < 0) {
+ die("cannot perform operation: pivot_root %s %s", scratch_dir,
+ dst);
+ }
+ // Unmount the self-bind mount over the scratch directory created earlier
+ // in the original root filesystem (which is now mounted on SC_HOSTFS_DIR).
+ // This way we can remove the temporary directory we created and "clean up"
+ // after ourselves nicely.
+ must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir);
+ debug("performing operation: umount %s", dst);
+ if (umount2(dst, 0) < 0) {
+ die("cannot perform operation: umount %s", dst);
+ }
+ // Remove the scratch directory. Note that we are using the path that is
+ // based on the old root filesystem as after pivot_root we cannot guarantee
+ // what is present at the same location normally. (It is probably an empty
+ // /tmp directory that is populated in another place).
+ debug("performing operation: rmdir %s", dst);
+ if (rmdir(scratch_dir) < 0) {
+ die("cannot perform operation: rmdir %s", dst);
+ };
+ // Make the old root filesystem recursively slave. This way operations
+ // performed in this mount namespace will not propagate to the peer group.
+ // This is another essential part of the confinement system.
+ debug("performing operation: mount --make-rslave %s", SC_HOSTFS_DIR);
+ if (mount("none", SC_HOSTFS_DIR, NULL, MS_REC | MS_SLAVE, NULL) < 0) {
+ die("cannot perform operation: mount --make-rslave %s",
+ SC_HOSTFS_DIR);
+ }
+ // Detach the redundant hostfs version of sysfs since it shows up in the
+ // mount table and software inspecting the mount table may become confused
+ // (eg, docker and LP:# 162601).
+ must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR);
+ debug("performing operation: umount --lazy %s", src);
+ if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) {
+ die("cannot perform operation: umount --lazy %s", src);
+ }
+ // Detach the redundant hostfs version of /dev since it shows up in the
+ // mount table and software inspecting the mount table may become confused.
+ must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR);
+ debug("performing operation: umount --lazy %s", src);
+ if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) {
+ die("cannot perform operation: umount --lazy %s", src);
+ }
+ // Detach the redundant hostfs version of /proc since it shows up in the
+ // mount table and software inspecting the mount table may become confused.
+ must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR);
+ debug("performing operation: umount --lazy %s", src);
+ if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) {
+ die("cannot perform operation: umount --lazy %s", src);
+ }
+}
+
+/**
+ * @path: a pathname where / replaced with '\0'.
+ * @offsetp: pointer to int showing which path segment was last seen.
+ * Updated on return to reflect the next segment.
+ * @fulllen: full original path length.
+ * Returns a pointer to the next path segment, or NULL if done.
+ */
+static char * __attribute__ ((used))
+ get_nextpath(char *path, size_t * offsetp, size_t fulllen)
+{
+ int offset = *offsetp;
+
+ if (offset >= fulllen)
+ return NULL;
+
+ while (offset < fulllen && path[offset] != '\0')
+ offset++;
+ while (offset < fulllen && path[offset] == '\0')
+ offset++;
+
+ *offsetp = offset;
+ return (offset < fulllen) ? &path[offset] : NULL;
+}
+
+/**
+ * Check that @subdir is a subdir of @dir.
+**/
+static bool __attribute__ ((used))
+ is_subdir(const char *subdir, const char *dir)
+{
+ size_t dirlen = strlen(dir);
+ size_t subdirlen = strlen(subdir);
+
+ // @dir has to be at least as long as @subdir
+ if (subdirlen < dirlen)
+ return false;
+ // @dir has to be a prefix of @subdir
+ if (strncmp(subdir, dir, dirlen) != 0)
+ return false;
+ // @dir can look like "path/" (that is, end with the directory separator).
+ // When that is the case then given the test above we can be sure @subdir
+ // is a real subdirectory.
+ if (dirlen > 0 && dir[dirlen - 1] == '/')
+ return true;
+ // @subdir can look like "path/stuff" and when the directory separator
+ // is exactly at the spot where @dir ends (that is, it was not caught
+ // by the test above) then @subdir is a real subdirectory.
+ if (subdir[dirlen] == '/' && dirlen > 0)
+ return true;
+ // If both @dir and @subdir have identical length then given that the
+ // prefix check above @subdir is a real subdirectory.
+ if (subdirlen == dirlen)
+ return true;
+ return false;
+}
+
+void sc_populate_mount_ns(const char *security_tag)
+{
+ // Get the current working directory before we start fiddling with
+ // mounts and possibly pivot_root. At the end of the whole process, we
+ // will try to re-locate to the same directory (if possible).
+ char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
+ vanilla_cwd = get_current_dir_name();
+ if (vanilla_cwd == NULL) {
+ die("cannot get the current working directory");
+ }
+ // Remember if we are on classic, some things behave differently there.
+ bool on_classic = is_running_on_classic_distribution();
+ if (on_classic) {
+ const struct sc_mount mounts[] = {
+ {"/dev"}, // because it contains devices on host OS
+ {"/etc"}, // because that's where /etc/resolv.conf lives, perhaps a bad idea
+ {"/home"}, // to support /home/*/snap and home interface
+ {"/root"}, // because that is $HOME for services
+ {"/proc"}, // fundamental filesystem
+ {"/sys"}, // fundamental filesystem
+ {"/tmp"}, // to get writable tmp
+ {"/var/snap"}, // to get access to global snap data
+ {"/var/lib/snapd"}, // to get access to snapd state and seccomp profiles
+ {"/var/tmp"}, // to get access to the other temporary directory
+ {"/run"}, // to get /run with sockets and what not
+ {"/lib/modules"}, // access to the modules of the running kernel
+ {"/usr/src"}, // FIXME: move to SecurityMounts in system-trace interface
+ {"/var/log"}, // FIXME: move to SecurityMounts in log-observe interface
+#ifdef MERGED_USR
+ {"/run/media", true}, // access to the users removable devices
+#else
+ {"/media", true}, // access to the users removable devices
+#endif // MERGED_USR
+ {"/run/netns", true}, // access to the 'ip netns' network namespaces
+ {},
+ };
+ struct sc_mount_config classic_config = {
+ .rootfs_dir = sc_get_outer_core_mount_point(),
+ .mounts = mounts,
+ .on_classic = true,
+ };
+ sc_bootstrap_mount_namespace(&classic_config);
+ } else {
+ // This is what happens on an all-snap system. The rootfs we start with
+ // is the real outer rootfs. There are no unidirectional bind mounts
+ // needed because everything is already OK. We still keep the
+ // bidirectional /media mount point so that snaps designed for mounting
+ // filesystems can use that space for whatever they need.
+ const struct sc_mount mounts[] = {
+ {"/media", true},
+ {"/run/netns", true},
+ {},
+ };
+ struct sc_mount_config all_snap_config = {
+ .rootfs_dir = "/",
+ .mounts = mounts,
+ };
+ sc_bootstrap_mount_namespace(&all_snap_config);
+ }
+
+ // set up private mounts
+ // TODO: rename this and fold it into bootstrap
+ setup_private_mount(security_tag);
+
+ // set up private /dev/pts
+ // TODO: fold this into bootstrap
+ setup_private_pts();
+
+ // setup quirks for specific snaps
+ if (on_classic) {
+ sc_setup_quirks();
+ }
+ // setup the security backend bind mounts
+ sc_setup_mount_profiles(security_tag);
+
+ // Try to re-locate back to vanilla working directory. This can fail
+ // because that directory is no longer present.
+ if (chdir(vanilla_cwd) != 0) {
+ debug("cannot remain in %s, moving to the void directory",
+ vanilla_cwd);
+ if (chdir(SC_VOID_DIR) != 0) {
+ die("cannot change directory to %s", SC_VOID_DIR);
+ }
+ debug("successfully moved to %s", SC_VOID_DIR);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_MOUNT_SUPPORT_H
+#define SNAP_MOUNT_SUPPORT_H
+
+/**
+ * Assuming a new mountspace, populate it accordingly.
+ *
+ * This function performs many internal tasks:
+ * - prepares and chroots into the core snap (on classic systems)
+ * - creates private /tmp
+ * - creates private /dev/pts
+ * - applies quirks for specific snaps (like LXD)
+ * - processes mount profiles
+ *
+ * The function will also try to preserve the current working directory but if
+ * this is impossible it will chdir to SC_VOID_DIR.
+ **/
+void sc_populate_mount_ns(const char *security_tag);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mountinfo.h"
+#include "mountinfo.c"
+
+#include <glib.h>
+
+static void test_parse_mountinfo_entry__sysfs()
+{
+ const char *line =
+ "19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 19);
+ g_assert_cmpint(entry->parent_id, ==, 25);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 18);
+ g_assert_cmpstr(entry->root, ==, "/");
+ g_assert_cmpstr(entry->mount_dir, ==, "/sys");
+ g_assert_cmpstr(entry->mount_opts, ==,
+ "rw,nosuid,nodev,noexec,relatime");
+ g_assert_cmpstr(entry->optional_fields, ==, "shared:7");
+ g_assert_cmpstr(entry->fs_type, ==, "sysfs");
+ g_assert_cmpstr(entry->mount_source, ==, "sysfs");
+ g_assert_cmpstr(entry->super_opts, ==, "rw");
+ g_assert_null(entry->next);
+}
+
+// Parse the /run/snapd/ns bind mount (over itself)
+// Note that /run is itself a tmpfs mount point.
+static void test_parse_mountinfo_entry__snapd_ns()
+{
+ const char *line =
+ "104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 104);
+ g_assert_cmpint(entry->parent_id, ==, 23);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 19);
+ g_assert_cmpstr(entry->root, ==, "/snapd/ns");
+ g_assert_cmpstr(entry->mount_dir, ==, "/run/snapd/ns");
+ g_assert_cmpstr(entry->mount_opts, ==, "rw,nosuid,noexec,relatime");
+ g_assert_cmpstr(entry->optional_fields, ==, "");
+ g_assert_cmpstr(entry->fs_type, ==, "tmpfs");
+ g_assert_cmpstr(entry->mount_source, ==, "tmpfs");
+ g_assert_cmpstr(entry->super_opts, ==, "rw,size=99840k,mode=755");
+ g_assert_null(entry->next);
+}
+
+static void test_parse_mountinfo_entry__snapd_mnt()
+{
+ const char *line =
+ "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 256);
+ g_assert_cmpint(entry->parent_id, ==, 104);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 3);
+ g_assert_cmpstr(entry->root, ==, "mnt:[4026532509]");
+ g_assert_cmpstr(entry->mount_dir, ==, "/run/snapd/ns/hello-world.mnt");
+ g_assert_cmpstr(entry->mount_opts, ==, "rw");
+ g_assert_cmpstr(entry->optional_fields, ==, "");
+ g_assert_cmpstr(entry->fs_type, ==, "nsfs");
+ g_assert_cmpstr(entry->mount_source, ==, "nsfs");
+ g_assert_cmpstr(entry->super_opts, ==, "rw");
+ g_assert_null(entry->next);
+}
+
+static void test_parse_mountinfo_entry__garbage()
+{
+ const char *line = "256 104 0:3";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_null(entry);
+}
+
+static void test_parse_mountinfo_entry__no_tags()
+{
+ const char *line =
+ "1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 1);
+ g_assert_cmpint(entry->parent_id, ==, 2);
+ g_assert_cmpint(entry->dev_major, ==, 3);
+ g_assert_cmpint(entry->dev_minor, ==, 4);
+ g_assert_cmpstr(entry->root, ==, "root");
+ g_assert_cmpstr(entry->mount_dir, ==, "mount-dir");
+ g_assert_cmpstr(entry->mount_opts, ==, "mount-opts");
+ g_assert_cmpstr(entry->optional_fields, ==, "");
+ g_assert_cmpstr(entry->fs_type, ==, "fs-type");
+ g_assert_cmpstr(entry->mount_source, ==, "mount-source");
+ g_assert_cmpstr(entry->super_opts, ==, "super-opts");
+ g_assert_null(entry->next);
+}
+
+static void test_parse_mountinfo_entry__one_tag()
+{
+ const char *line =
+ "1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 1);
+ g_assert_cmpint(entry->parent_id, ==, 2);
+ g_assert_cmpint(entry->dev_major, ==, 3);
+ g_assert_cmpint(entry->dev_minor, ==, 4);
+ g_assert_cmpstr(entry->root, ==, "root");
+ g_assert_cmpstr(entry->mount_dir, ==, "mount-dir");
+ g_assert_cmpstr(entry->mount_opts, ==, "mount-opts");
+ g_assert_cmpstr(entry->optional_fields, ==, "tag:1");
+ g_assert_cmpstr(entry->fs_type, ==, "fs-type");
+ g_assert_cmpstr(entry->mount_source, ==, "mount-source");
+ g_assert_cmpstr(entry->super_opts, ==, "super-opts");
+ g_assert_null(entry->next);
+}
+
+static void test_parse_mountinfo_entry__two_tags()
+{
+ const char *line =
+ "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 - fs-type mount-source super-opts";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 1);
+ g_assert_cmpint(entry->parent_id, ==, 2);
+ g_assert_cmpint(entry->dev_major, ==, 3);
+ g_assert_cmpint(entry->dev_minor, ==, 4);
+ g_assert_cmpstr(entry->root, ==, "root");
+ g_assert_cmpstr(entry->mount_dir, ==, "mount-dir");
+ g_assert_cmpstr(entry->mount_opts, ==, "mount-opts");
+ g_assert_cmpstr(entry->optional_fields, ==, "tag:1 tag:2");
+ g_assert_cmpstr(entry->fs_type, ==, "fs-type");
+ g_assert_cmpstr(entry->mount_source, ==, "mount-source");
+ g_assert_cmpstr(entry->super_opts, ==, "super-opts");
+ g_assert_null(entry->next);
+}
+
+static void test_accessor_funcs()
+{
+ const char *line =
+ "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw";
+ struct mountinfo_entry *entry = parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry);
+ g_assert_cmpint(mountinfo_entry_mount_id(entry), ==, 256);
+ g_assert_cmpint(mountinfo_entry_parent_id(entry), ==, 104);
+ g_assert_cmpint(mountinfo_entry_dev_major(entry), ==, 0);
+ g_assert_cmpint(mountinfo_entry_dev_minor(entry), ==, 3);
+
+ g_assert_cmpstr(mountinfo_entry_root(entry), ==, "mnt:[4026532509]");
+ g_assert_cmpstr(mountinfo_entry_mount_dir(entry), ==,
+ "/run/snapd/ns/hello-world.mnt");
+ g_assert_cmpstr(mountinfo_entry_mount_opts(entry), ==, "rw");
+ g_assert_cmpstr(mountinfo_entry_optional_fields(entry), ==, "");
+ g_assert_cmpstr(mountinfo_entry_fs_type(entry), ==, "nsfs");
+ g_assert_cmpstr(mountinfo_entry_mount_source(entry), ==, "nsfs");
+ g_assert_cmpstr(mountinfo_entry_super_opts(entry), ==, "rw");
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/sysfs",
+ test_parse_mountinfo_entry__sysfs);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/snapd-ns",
+ test_parse_mountinfo_entry__snapd_ns);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/snapd-mnt",
+ test_parse_mountinfo_entry__snapd_mnt);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/garbage",
+ test_parse_mountinfo_entry__garbage);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/no_tags",
+ test_parse_mountinfo_entry__no_tags);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/one_tags",
+ test_parse_mountinfo_entry__one_tag);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/two_tags",
+ test_parse_mountinfo_entry__two_tags);
+ g_test_add_func("/mountinfo/accessor_funcs", test_accessor_funcs);
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "mountinfo.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct mountinfo {
+ struct mountinfo_entry *first;
+};
+
+struct mountinfo_entry {
+ int mount_id;
+ int parent_id;
+ unsigned dev_major, dev_minor;
+ char *root;
+ char *mount_dir;
+ char *mount_opts;
+ char *optional_fields;
+ char *fs_type;
+ char *mount_source;
+ char *super_opts;
+
+ struct mountinfo_entry *next;
+ // Buffer holding all of the text data above.
+ //
+ // The buffer must be the last element of the structure. It is allocated
+ // along with the structure itself and does not need to be freed
+ // separately.
+ char line_buf[0];
+};
+
+/**
+ * Parse a single mountinfo entry (line).
+ *
+ * The format, described by Linux kernel documentation, is as follows:
+ *
+ * 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
+ * (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
+ *
+ * (1) mount ID: unique identifier of the mount (may be reused after umount)
+ * (2) parent ID: ID of parent (or of self for the top of the mount tree)
+ * (3) major:minor: value of st_dev for files on filesystem
+ * (4) root: root of the mount within the filesystem
+ * (5) mount point: mount point relative to the process's root
+ * (6) mount options: per mount options
+ * (7) optional fields: zero or more fields of the form "tag[:value]"
+ * (8) separator: marks the end of the optional fields
+ * (9) filesystem type: name of filesystem of the form "type[.subtype]"
+ * (10) mount source: filesystem specific information or "none"
+ * (11) super options: per super block options
+ **/
+static struct mountinfo_entry *parse_mountinfo_entry(const char *line)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Free a mountinfo structure and all its entries.
+ **/
+static void free_mountinfo(struct mountinfo *info)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Free a mountinfo entry.
+ **/
+static void free_mountinfo_entry(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+static void cleanup_fclose(FILE ** ptr);
+static void cleanup_free(char **ptr);
+
+struct mountinfo_entry *first_mountinfo_entry(struct mountinfo *info)
+{
+ return info->first;
+}
+
+struct mountinfo_entry *next_mountinfo_entry(struct mountinfo_entry
+ *entry)
+{
+ return entry->next;
+}
+
+int mountinfo_entry_mount_id(struct mountinfo_entry *entry)
+{
+ return entry->mount_id;
+}
+
+int mountinfo_entry_parent_id(struct mountinfo_entry *entry)
+{
+ return entry->parent_id;
+}
+
+unsigned mountinfo_entry_dev_major(struct mountinfo_entry *entry)
+{
+ return entry->dev_major;
+}
+
+unsigned mountinfo_entry_dev_minor(struct mountinfo_entry *entry)
+{
+ return entry->dev_minor;
+}
+
+const char *mountinfo_entry_root(struct mountinfo_entry *entry)
+{
+ return entry->root;
+}
+
+const char *mountinfo_entry_mount_dir(struct mountinfo_entry *entry)
+{
+ return entry->mount_dir;
+}
+
+const char *mountinfo_entry_mount_opts(struct mountinfo_entry *entry)
+{
+ return entry->mount_opts;
+}
+
+const char *mountinfo_entry_optional_fields(struct mountinfo_entry *entry)
+{
+ return entry->optional_fields;
+}
+
+const char *mountinfo_entry_fs_type(struct mountinfo_entry *entry)
+{
+ return entry->fs_type;
+}
+
+const char *mountinfo_entry_mount_source(struct mountinfo_entry *entry)
+{
+ return entry->mount_source;
+}
+
+const char *mountinfo_entry_super_opts(struct mountinfo_entry *entry)
+{
+ return entry->super_opts;
+}
+
+struct mountinfo *parse_mountinfo(const char *fname)
+{
+ struct mountinfo *info = calloc(1, sizeof *info);
+ if (info == NULL) {
+ return NULL;
+ }
+ if (fname == NULL) {
+ fname = "/proc/self/mountinfo";
+ }
+ FILE *f __attribute__ ((cleanup(cleanup_fclose))) = fopen(fname, "rt");
+ if (f == NULL) {
+ free(info);
+ return NULL;
+ }
+ char *line __attribute__ ((cleanup(cleanup_free))) = NULL;
+ size_t line_size = 0;
+ struct mountinfo_entry *entry, *last = NULL;
+ for (;;) {
+ errno = 0;
+ if (getline(&line, &line_size, f) == -1) {
+ if (errno != 0) {
+ free_mountinfo(info);
+ return NULL;
+ }
+ break;
+ };
+ entry = parse_mountinfo_entry(line);
+ if (entry == NULL) {
+ free_mountinfo(info);
+ return NULL;
+ }
+ if (last != NULL) {
+ last->next = entry;
+ } else {
+ info->first = entry;
+ }
+ last = entry;
+ }
+ return info;
+}
+
+static struct mountinfo_entry *parse_mountinfo_entry(const char *line)
+{
+ // NOTE: the mountinfo structure is allocated along with enough extra
+ // storage to hold the whole line we are parsing. This is used as backing
+ // store for all text fields.
+ //
+ // The idea is that since the line has a given length and we are only after
+ // set of substrings we can easily predict the amount of required space
+ // (after all, it is just a set of non-overlapping substrings) and append
+ // it to the allocated entry structure.
+ //
+ // The parsing code below, specifically parse_next_string_field(), uses
+ // this extra memory to hold data parsed from the original line. In the
+ // end, the result is similar to using strtok except that the source and
+ // destination buffers are separate.
+ struct mountinfo_entry *entry =
+ calloc(1, sizeof *entry + strlen(line) + 1);
+ if (entry == NULL) {
+ return NULL;
+ }
+ int nscanned;
+ int offset, total_offset = 0;
+ nscanned = sscanf(line, "%d %d %u:%u %n",
+ &entry->mount_id, &entry->parent_id,
+ &entry->dev_major, &entry->dev_minor, &offset);
+ if (nscanned != 4)
+ goto fail;
+ total_offset += offset;
+ int total_used = 0;
+ char *parse_next_string_field() {
+ char *field = &entry->line_buf[0] + total_used;
+ nscanned = sscanf(line + total_offset, "%s %n", field, &offset);
+ if (nscanned != 1)
+ return NULL;
+ total_offset += offset;
+ total_used += offset + 1;
+ return field;
+ }
+ if ((entry->root = parse_next_string_field()) == NULL)
+ goto fail;
+ if ((entry->mount_dir = parse_next_string_field()) == NULL)
+ goto fail;
+ if ((entry->mount_opts = parse_next_string_field()) == NULL)
+ goto fail;
+ entry->optional_fields = &entry->line_buf[0] + total_used++;
+ // NOTE: This ensures that optional_fields is never NULL. If this changes,
+ // must adjust all callers of parse_mountinfo_entry() accordingly.
+ strcpy(entry->optional_fields, "");
+ for (;;) {
+ char *opt_field = parse_next_string_field();
+ if (opt_field == NULL)
+ goto fail;
+ if (strcmp(opt_field, "-") == 0) {
+ break;
+ }
+ if (*entry->optional_fields) {
+ strcat(entry->optional_fields, " ");
+ }
+ strcat(entry->optional_fields, opt_field);
+ }
+ if ((entry->fs_type = parse_next_string_field()) == NULL)
+ goto fail;
+ if ((entry->mount_source = parse_next_string_field()) == NULL)
+ goto fail;
+ if ((entry->super_opts = parse_next_string_field()) == NULL)
+ goto fail;
+ return entry;
+ fail:
+ free(entry);
+ return NULL;
+}
+
+void cleanup_mountinfo(struct mountinfo **ptr)
+{
+ if (*ptr != NULL) {
+ free_mountinfo(*ptr);
+ *ptr = NULL;
+ }
+}
+
+static void free_mountinfo(struct mountinfo *info)
+{
+ struct mountinfo_entry *entry, *next;
+ for (entry = info->first; entry != NULL; entry = next) {
+ next = entry->next;
+ free_mountinfo_entry(entry);
+ }
+ free(info);
+}
+
+static void free_mountinfo_entry(struct mountinfo_entry *entry)
+{
+ free(entry);
+}
+
+static void cleanup_fclose(FILE ** ptr)
+{
+ fclose(*ptr);
+}
+
+static void cleanup_free(char **ptr)
+{
+ free(*ptr);
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SC_MOUNTINFO_H
+#define SC_MOUNTINFO_H
+
+/**
+ * Structure describing entire /proc/self/mountinfo file
+ **/
+struct mountinfo;
+
+/**
+ * Structure describing a single entry in /proc/self/mountinfo
+ **/
+struct mountinfo_entry;
+
+/**
+ * Parse a file in according to mountinfo syntax.
+ *
+ * The argument can be used to parse an arbitrary file. NULL can be used to
+ * implicitly parse /proc/self/mountinfo, that is the mount information
+ * associated with the current process.
+ **/
+struct mountinfo *parse_mountinfo(const char *fname);
+
+/**
+ * Free a mountinfo structure.
+ *
+ * This function is designed to be used with __attribute__((cleanup)) so it
+ * takes a pointer to the freed object (which is also a pointer).
+ **/
+void cleanup_mountinfo(struct mountinfo **ptr) __attribute__ ((nonnull(1)));
+
+/**
+ * Get the first mountinfo entry.
+ *
+ * The returned value may be NULL if the parsed file contained no entries. The
+ * returned value is bound to the lifecycle of the whole mountinfo structure
+ * and should not be freed explicitly.
+ **/
+struct mountinfo_entry *first_mountinfo_entry(struct mountinfo *info)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the next mountinfo entry.
+ *
+ * The returned value is a pointer to the next mountinfo entry or NULL if this
+ * was the last entry. The returned value is bound to the lifecycle of the
+ * whole mountinfo structure and should not be freed explicitly.
+ **/
+struct mountinfo_entry *next_mountinfo_entry(struct mountinfo_entry
+ *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the mount identifier of a given mount entry.
+ **/
+int mountinfo_entry_mount_id(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the parent mount identifier of a given mount entry.
+ **/
+int mountinfo_entry_parent_id(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+unsigned mountinfo_entry_dev_major(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+unsigned mountinfo_entry_dev_minor(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the root directory of a given mount entry.
+ **/
+const char *mountinfo_entry_root(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the mount point of a given mount entry.
+ **/
+const char *mountinfo_entry_mount_dir(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the mount options of a given mount entry.
+ **/
+const char *mountinfo_entry_mount_opts(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get optional tagged data associated of a given mount entry.
+ *
+ * The return value is a string (possibly empty but never NULL) in the format
+ * tag[:value]. Known tags are:
+ *
+ * "shared:X":
+ * mount is shared in peer group X
+ * "master:X":
+ * mount is slave to peer group X
+ * "propagate_from:X"
+ * mount is slave and receives propagation from peer group X (*)
+ * "unbindable":
+ * mount is unbindable
+ *
+ * (*) X is the closest dominant peer group under the process's root.
+ * If X is the immediate master of the mount, or if there's no dominant peer
+ * group under the same root, then only the "master:X" field is present and not
+ * the "propagate_from:X" field.
+ **/
+const char *mountinfo_entry_optional_fields(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the file system type of a given mount entry.
+ **/
+const char *mountinfo_entry_fs_type(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the source of a given mount entry.
+ **/
+const char *mountinfo_entry_mount_source(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+/**
+ * Get the super block options of a given mount entry.
+ **/
+const char *mountinfo_entry_super_opts(struct mountinfo_entry *entry)
+ __attribute__ ((nonnull(1)));
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "ns-support.h"
+#include "ns-support.c"
+
+#include "cleanup-funcs.h"
+
+#include <errno.h>
+#include <linux/magic.h> // for NSFS_MAGIC
+#include <sys/utsname.h>
+#include <sys/vfs.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+// Set alternate namespace directory
+static void sc_set_ns_dir(const char *dir)
+{
+ sc_ns_dir = dir;
+}
+
+// Shell-out to "rm -rf -- $dir" as long as $dir is in /tmp.
+static void rm_rf_tmp(const char *dir)
+{
+ // Sanity check, don't remove anything that's not in the temporary
+ // directory. This is here to prevent unintended data loss.
+ if (!g_str_has_prefix(dir, "/tmp/"))
+ die("refusing to remove: %s", dir);
+ const gchar *working_directory = NULL;
+ gchar **argv = NULL;
+ gchar **envp = NULL;
+ GSpawnFlags flags = G_SPAWN_SEARCH_PATH;
+ GSpawnChildSetupFunc child_setup = NULL;
+ gpointer user_data = NULL;
+ gchar **standard_output = NULL;
+ gchar **standard_error = NULL;
+ gint exit_status = 0;
+ GError *error = NULL;
+
+ argv = calloc(5, sizeof *argv);
+ if (argv == NULL)
+ die("cannot allocate command argument array");
+ argv[0] = g_strdup("rm");
+ if (argv[0] == NULL)
+ die("cannot allocate memory");
+ argv[1] = g_strdup("-rf");
+ if (argv[1] == NULL)
+ die("cannot allocate memory");
+ argv[2] = g_strdup("--");
+ if (argv[2] == NULL)
+ die("cannot allocate memory");
+ argv[3] = g_strdup(dir);
+ if (argv[3] == NULL)
+ die("cannot allocate memory");
+ argv[4] = NULL;
+ g_assert_true(g_spawn_sync
+ (working_directory, argv, envp, flags, child_setup,
+ user_data, standard_output, standard_error, &exit_status,
+ &error));
+ g_assert_true(g_spawn_check_exit_status(exit_status, NULL));
+ if (error != NULL) {
+ g_test_message("cannot remove temporary directory: %s\n",
+ error->message);
+ g_error_free(error);
+ }
+ g_free(argv[0]);
+ g_free(argv[1]);
+ g_free(argv[2]);
+ g_free(argv[3]);
+ g_free(argv);
+}
+
+// Check that rm_rf_tmp doesn't remove things outside of /tmp
+static void test_rm_rf_tmp()
+{
+ if (access("/nonexistent", F_OK) == 0) {
+ g_test_message
+ ("/nonexistent exists but this test doesn't want it to");
+ g_test_fail();
+ return;
+ }
+ if (g_test_subprocess()) {
+ rm_rf_tmp("/nonexistent");
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+}
+
+// Use temporary directory for namespace groups.
+//
+// The directory is automatically reset to the real value at the end of the
+// test.
+static const char *sc_test_use_fake_ns_dir()
+{
+ char *ns_dir = NULL;
+ if (g_test_subprocess()) {
+ // Check if the environment variable is set. If so then someone is already
+ // managing the temporary directory and we should not create a new one.
+ ns_dir = getenv("SNAP_CONFINE_NS_DIR");
+ g_assert_nonnull(ns_dir);
+ } else {
+ ns_dir = g_dir_make_tmp(NULL, NULL);
+ g_assert_nonnull(ns_dir);
+ g_test_queue_free(ns_dir);
+ g_assert_cmpint(setenv("SNAP_CONFINE_NS_DIR", ns_dir, 0), ==,
+ 0);
+ g_test_queue_destroy((GDestroyNotify) unsetenv,
+ "SNAP_CONFINE_NS_DIR");
+ g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, ns_dir);
+ }
+ g_test_queue_destroy((GDestroyNotify) sc_set_ns_dir, SC_NS_DIR);
+ sc_set_ns_dir(ns_dir);
+ return ns_dir;
+}
+
+// Check that allocating a namespace group sets up internal data structures to
+// safe values.
+static void test_sc_alloc_ns_group()
+{
+ struct sc_ns_group *group = NULL;
+ group = sc_alloc_ns_group();
+ g_test_queue_free(group);
+ g_assert_nonnull(group);
+ g_assert_cmpint(group->dir_fd, ==, -1);
+ g_assert_cmpint(group->lock_fd, ==, -1);
+ g_assert_cmpint(group->event_fd, ==, -1);
+ g_assert_cmpint(group->child, ==, 0);
+ g_assert_cmpint(group->should_populate, ==, false);
+ g_assert_null(group->name);
+}
+
+// Initialize a namespace group.
+//
+// The group is automatically destroyed at the end of the test.
+static struct sc_ns_group *sc_test_open_ns_group(const char *group_name)
+{
+ // Initialize a namespace group
+ struct sc_ns_group *group = NULL;
+ if (group_name == NULL) {
+ group_name = "test-group";
+ }
+ group = sc_open_ns_group(group_name, 0);
+ g_test_queue_destroy((GDestroyNotify) sc_close_ns_group, group);
+ // Check if the returned group data looks okay
+ g_assert_nonnull(group);
+ g_assert_cmpint(group->dir_fd, !=, -1);
+ g_assert_cmpint(group->lock_fd, !=, -1);
+ g_assert_cmpint(group->event_fd, ==, -1);
+ g_assert_cmpint(group->child, ==, 0);
+ g_assert_cmpint(group->should_populate, ==, false);
+ g_assert_cmpstr(group->name, ==, group_name);
+ return group;
+}
+
+// Check that initializing a namespace group creates the appropriate
+// filesystem structure and obtains open file descriptors for the lock.
+static void test_sc_open_ns_group()
+{
+ const char *ns_dir = sc_test_use_fake_ns_dir();
+ struct sc_ns_group *group = sc_test_open_ns_group(NULL);
+ // Check that the group directory exists
+ g_assert_true(g_file_test
+ (ns_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR));
+ // Check that the lock file exists
+ char *lock_file __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
+ lock_file =
+ g_strdup_printf("%s/%s%s", ns_dir, group->name, SC_NS_LOCK_FILE);
+ g_assert_true(g_file_test
+ (lock_file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR));
+}
+
+static void test_sc_open_ns_group_graceful()
+{
+ sc_set_ns_dir("/nonexistent");
+ g_test_queue_destroy((GDestroyNotify) sc_set_ns_dir, SC_NS_DIR);
+ struct sc_ns_group *group =
+ sc_open_ns_group("foo", SC_NS_FAIL_GRACEFULLY);
+ g_assert_null(group);
+}
+
+static void test_sc_lock_ns_mutex_precondition()
+{
+ sc_test_use_fake_ns_dir();
+ if (g_test_subprocess()) {
+ struct sc_ns_group *group = sc_alloc_ns_group();
+ g_test_queue_free(group);
+ // Try to lock the mutex, this should abort because we never opened the
+ // lock file and don't have a valid file descriptor.
+ sc_lock_ns_mutex(group);
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+}
+
+static void test_sc_unlock_ns_mutex_precondition()
+{
+ sc_test_use_fake_ns_dir();
+ if (g_test_subprocess()) {
+ struct sc_ns_group *group = sc_alloc_ns_group();
+ g_test_queue_free(group);
+ // Try to unlock the mutex, this should abort because we never opened the
+ // lock file and don't have a valid file descriptor.
+ sc_unlock_ns_mutex(group);
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+}
+
+// Check that locking a namespace actually flock's the mutex with LOCK_EX
+static void test_sc_lock_unlock_ns_mutex()
+{
+ const char *ns_dir = sc_test_use_fake_ns_dir();
+ struct sc_ns_group *group = sc_test_open_ns_group(NULL);
+ // Lock the namespace group mutex
+ sc_lock_ns_mutex(group);
+ // Construct the name of the lock file
+ char *lock_file __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
+ lock_file =
+ g_strdup_printf("%s/%s%s", ns_dir, group->name, SC_NS_LOCK_FILE);
+ // Open the lock file again to obtain a separate file descriptor.
+ // According to flock(2) locks are associated with an open file table entry
+ // so this descriptor will be separate and can compete for the same lock.
+ int lock_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
+ lock_fd = open(lock_file, O_RDWR | O_CLOEXEC | O_NOFOLLOW);
+ g_assert_cmpint(lock_fd, !=, -1);
+ // The non-blocking lock operation should fail with EWOULDBLOCK as the lock
+ // file is locked by sc_nlock_ns_mutex() already.
+ int err = flock(lock_fd, LOCK_EX | LOCK_NB);
+ int saved_errno = errno;
+ g_assert_cmpint(err, ==, -1);
+ g_assert_cmpint(saved_errno, ==, EWOULDBLOCK);
+ // Unlock the namespace group mutex
+ sc_unlock_ns_mutex(group);
+ // Re-attempt the locking operation. This time it should succeed.
+ err = flock(lock_fd, LOCK_EX | LOCK_NB);
+ g_assert_cmpint(err, ==, 0);
+}
+
+static void unmount_dir(void *dir)
+{
+ umount(dir);
+}
+
+static void test_sc_is_ns_group_dir_private()
+{
+ if (geteuid() != 0) {
+ g_test_skip("this test needs to run as root");
+ return;
+ }
+ const char *ns_dir = sc_test_use_fake_ns_dir();
+ g_test_queue_destroy(unmount_dir, (char *)ns_dir);
+
+ if (g_test_subprocess()) {
+ // The temporary directory should not be private initially
+ g_assert_false(sc_is_ns_group_dir_private());
+
+ /// do what "mount --bind /foo /foo; mount --make-private /foo" does.
+ int err;
+ err = mount(ns_dir, ns_dir, NULL, MS_BIND, NULL);
+ g_assert_cmpint(err, ==, 0);
+ err = mount(NULL, ns_dir, NULL, MS_PRIVATE, NULL);
+ g_assert_cmpint(err, ==, 0);
+
+ // The temporary directory should now be private
+ g_assert_true(sc_is_ns_group_dir_private());
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR);
+ g_test_trap_assert_passed();
+}
+
+static void test_sc_initialize_ns_groups()
+{
+ if (geteuid() != 0) {
+ g_test_skip("this test needs to run as root");
+ return;
+ }
+ // NOTE: this is g_test_subprocess aware!
+ const char *ns_dir = sc_test_use_fake_ns_dir();
+ g_test_queue_destroy(unmount_dir, (char *)ns_dir);
+ if (g_test_subprocess()) {
+ // Initialize namespace groups using a fake directory.
+ sc_initialize_ns_groups();
+
+ // Check that the fake directory is now a private mount.
+ g_assert_true(sc_is_ns_group_dir_private());
+
+ // Check that the lock file did not leak unclosed.
+
+ // Construct the name of the lock file
+ char *lock_file __attribute__ ((cleanup(sc_cleanup_string))) =
+ NULL;
+ lock_file =
+ g_strdup_printf("%s/%s", sc_ns_dir, SC_NS_LOCK_FILE);
+ // Attempt to open and lock the lock file.
+ int lock_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
+ lock_fd = open(lock_file, O_RDWR | O_CLOEXEC | O_NOFOLLOW);
+ g_assert_cmpint(lock_fd, !=, -1);
+ // The non-blocking lock operation should not fail
+ int err = flock(lock_fd, LOCK_EX | LOCK_NB);
+ g_assert_cmpint(err, ==, 0);
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR);
+ g_test_trap_assert_passed();
+}
+
+// Sanity check, ensure that the namespace filesystem identifier is what we
+// expect, aka NSFS_MAGIC.
+static void test_nsfs_fs_id()
+{
+ struct utsname uts;
+ if (uname(&uts) < 0) {
+ g_test_message("cannot use uname(2)");
+ g_test_fail();
+ return;
+ }
+ int major, minor;
+ if (sscanf(uts.release, "%d.%d", &major, &minor) != 2) {
+ g_test_message("cannot use sscanf(2) to parse kernel release");
+ g_test_fail();
+ return;
+ }
+ if (major < 3 || (major == 3 && minor < 19)) {
+ g_test_skip("this test needs kernel 3.19+");
+ return;
+ }
+ struct statfs buf;
+ int err = statfs("/proc/self/ns/mnt", &buf);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_cmpint(buf.f_type, ==, NSFS_MAGIC);
+}
+
+static void test_sc_enable_sanity_timeout()
+{
+ if (g_test_subprocess()) {
+ sc_enable_sanity_timeout();
+ debug("waiting...");
+ usleep(4 * G_USEC_PER_SEC);
+ debug("woke up");
+ sc_disable_sanity_timeout();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 5 * G_USEC_PER_SEC,
+ G_TEST_SUBPROCESS_INHERIT_STDERR);
+ g_test_trap_assert_failed();
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/internal/rm_rf_tmp", test_rm_rf_tmp);
+ g_test_add_func("/ns/sc_enable_sanity_timeout",
+ test_sc_enable_sanity_timeout);
+ g_test_add_func("/ns/sc_alloc_ns_group", test_sc_alloc_ns_group);
+ g_test_add_func("/ns/sc_open_ns_group", test_sc_open_ns_group);
+ g_test_add_func("/ns/sc_open_ns_group/graceful",
+ test_sc_open_ns_group_graceful);
+ g_test_add_func("/ns/sc_lock_unlock_ns_mutex",
+ test_sc_lock_unlock_ns_mutex);
+ g_test_add_func("/ns/sc_lock_ns_mutex/precondition",
+ test_sc_lock_ns_mutex_precondition);
+ g_test_add_func("/ns/sc_unlock_ns_mutex/precondition",
+ test_sc_unlock_ns_mutex_precondition);
+ g_test_add_func("/ns/nsfs_fs_id", test_nsfs_fs_id);
+ g_test_add_func("/system/ns/sc_is_ns_group_dir_private",
+ test_sc_is_ns_group_dir_private);
+ g_test_add_func("/system/ns/sc_initialize_ns_groups",
+ test_sc_initialize_ns_groups);
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "ns-support.h"
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/magic.h>
+#include <sched.h>
+#include <signal.h>
+#include <string.h>
+#include <sys/eventfd.h>
+#include <sys/file.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "utils.h"
+#include "user-support.h"
+#include "mountinfo.h"
+#include "cleanup-funcs.h"
+
+/**
+ * Flag indicating that a sanity timeout has expired.
+ **/
+static volatile sig_atomic_t sanity_timeout_expired = 0;
+
+/**
+ * Signal handler for SIGALRM that sets sanity_timeout_expired flag to 1.
+ **/
+static void sc_SIGALRM_handler(int signum)
+{
+ sanity_timeout_expired = 1;
+}
+
+/**
+ * Enable a sanity-check timeout.
+ *
+ * The timeout is based on good-old alarm(2) and is intended to break a
+ * suspended system call, such as flock, after a few seconds. The built-in
+ * timeout is primed for three seconds. After that any sleeping system calls
+ * are interrupted and a flag is set.
+ *
+ * The call should be paired with sc_disable_sanity_check_timeout() that
+ * disables the alarm and acts on the flag, aborting the process if the timeout
+ * gets exceeded.
+ **/
+static void sc_enable_sanity_timeout()
+{
+ sanity_timeout_expired = 0;
+ struct sigaction act = {.sa_handler = sc_SIGALRM_handler };
+ if (sigemptyset(&act.sa_mask) < 0) {
+ die("cannot initialize POSIX signal set");
+ }
+ // NOTE: we are using sigaction so that we can explicitly control signal
+ // flags and *not* pass the SA_RESTART flag. The intent is so that any
+ // system call we may be sleeping on to get interrupted.
+ act.sa_flags = 0;
+ if (sigaction(SIGALRM, &act, NULL) < 0) {
+ die("cannot install signal handler for SIGALRM");
+ }
+ alarm(3);
+ debug("sanity timeout initialized and set for three seconds");
+}
+
+/**
+ * Disable sanity-check timeout and abort the process if it expired.
+ *
+ * This call has to be paired with sc_enable_sanity_timeout(), see the function
+ * description for more details.
+ **/
+static void sc_disable_sanity_timeout()
+{
+ if (sanity_timeout_expired) {
+ die("sanity timeout expired");
+ }
+ alarm(0);
+ struct sigaction act = {.sa_handler = SIG_DFL };
+ if (sigemptyset(&act.sa_mask) < 0) {
+ die("cannot initialize POSIX signal set");
+ }
+ if (sigaction(SIGALRM, &act, NULL) < 0) {
+ die("cannot uninstall signal handler for SIGALRM");
+ }
+ debug("sanity timeout reset and disabled");
+}
+
+/*!
+ * The void directory.
+ *
+ * Snap confine moves to that directory in case it cannot retain the current
+ * working directory across the pivot_root call.
+ **/
+#define SC_VOID_DIR "/var/lib/snapd/void"
+
+/**
+ * Directory where snap-confine keeps namespace files.
+ **/
+#define SC_NS_DIR "/run/snapd/ns"
+
+/**
+ * Effective value of SC_NS_DIR.
+ *
+ * We use 'const char *' so we can update sc_ns_dir in the testsuite
+ **/
+static const char *sc_ns_dir = SC_NS_DIR;
+
+/**
+ * Name of the lock file associated with SC_NS_DIR.
+ * and a given group identifier (typically SNAP_NAME).
+ **/
+#define SC_NS_LOCK_FILE ".lock"
+
+/**
+ * Name of the preserved mount namespace associated with SC_NS_DIR
+ * and a given group identifier (typically SNAP_NAME).
+ **/
+#define SC_NS_MNT_FILE ".mnt"
+
+/**
+ * Read /proc/self/mountinfo and check if /run/snapd/ns is a private bind mount.
+ *
+ * We do this because /run/snapd/ns cannot be shared with any other peers as per:
+ * https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
+ **/
+static bool sc_is_ns_group_dir_private()
+{
+ struct mountinfo *info
+ __attribute__ ((cleanup(cleanup_mountinfo))) = NULL;
+ info = parse_mountinfo(NULL);
+ if (info == NULL) {
+ die("cannot parse /proc/self/mountinfo");
+ }
+ struct mountinfo_entry *entry = first_mountinfo_entry(info);
+ while (entry != NULL) {
+ const char *mount_dir = mountinfo_entry_mount_dir(entry);
+ const char *optional_fields =
+ mountinfo_entry_optional_fields(entry);
+ if (strcmp(mount_dir, sc_ns_dir) == 0
+ && strcmp(optional_fields, "") == 0) {
+ // If /run/snapd/ns has no optional fields, we know it is mounted
+ // private and there is nothing else to do.
+ return true;
+ }
+ entry = next_mountinfo_entry(entry);
+ }
+ return false;
+}
+
+void sc_initialize_ns_groups()
+{
+ debug("creating namespace group directory %s", sc_ns_dir);
+ if (sc_nonfatal_mkpath(sc_ns_dir, 0755) < 0) {
+ die("cannot create namespace group directory %s", sc_ns_dir);
+ }
+ debug("opening namespace group directory %s", sc_ns_dir);
+ int dir_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
+ dir_fd = open(sc_ns_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW);
+ if (dir_fd < 0) {
+ die("cannot open namespace group directory");
+ }
+ debug("opening lock file for group directory");
+ int lock_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
+ lock_fd = openat(dir_fd,
+ SC_NS_LOCK_FILE,
+ O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
+ if (lock_fd < 0) {
+ die("cannot open lock file for namespace group directory");
+ }
+ debug("locking the namespace group directory");
+ sc_enable_sanity_timeout();
+ if (flock(lock_fd, LOCK_EX) < 0) {
+ die("cannot acquire exclusive lock for namespace group directory");
+ }
+ sc_disable_sanity_timeout();
+ if (!sc_is_ns_group_dir_private()) {
+ debug
+ ("bind mounting the namespace group directory over itself");
+ if (mount(sc_ns_dir, sc_ns_dir, NULL, MS_BIND | MS_REC, NULL) <
+ 0) {
+ die("cannot bind mount namespace group directory over itself");
+ }
+ debug
+ ("making the namespace group directory mount point private");
+ if (mount(NULL, sc_ns_dir, NULL, MS_PRIVATE, NULL) < 0) {
+ die("cannot make the namespace group directory mount point private");
+ }
+ } else {
+ debug
+ ("namespace group directory does not require intialization");
+ }
+ debug("unlocking the namespace group directory");
+ if (flock(lock_fd, LOCK_UN) < 0) {
+ die("cannot release lock for namespace control directory");
+ }
+}
+
+struct sc_ns_group {
+ // Name of the namespace group ($SNAP_NAME).
+ char *name;
+ // Descriptor to the namespace group control directory. This descriptor is
+ // opened with O_PATH|O_DIRECTORY so it's only used for openat() calls.
+ int dir_fd;
+ // Descriptor to a namespace-specific lock file (i.e. $SNAP_NAME.lock).
+ int lock_fd;
+ // Descriptor to an eventfd that is used to notify the child that it can
+ // now complete its job and exit.
+ int event_fd;
+ // Identifier of the child process that is used during the one-time (per
+ // group) initialization and capture process.
+ pid_t child;
+ // Flag set when this process created a fresh namespace should populate it.
+ bool should_populate;
+};
+
+static struct sc_ns_group *sc_alloc_ns_group()
+{
+ struct sc_ns_group *group = calloc(1, sizeof *group);
+ if (group == NULL) {
+ die("cannot allocate memory for namespace group");
+ }
+ group->dir_fd = -1;
+ group->lock_fd = -1;
+ group->event_fd = -1;
+ // Redundant with calloc but some functions check for the non-zero value so
+ // I'd like to keep this explicit in the code.
+ group->child = 0;
+ return group;
+}
+
+struct sc_ns_group *sc_open_ns_group(const char *group_name,
+ const unsigned flags)
+{
+ struct sc_ns_group *group = sc_alloc_ns_group();
+ debug("opening namespace group directory %s", sc_ns_dir);
+ group->dir_fd =
+ open(sc_ns_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW);
+ if (group->dir_fd < 0) {
+ if (flags & SC_NS_FAIL_GRACEFULLY && errno == ENOENT) {
+ free(group);
+ return NULL;
+ }
+ die("cannot open directory for namespace group %s", group_name);
+ }
+ char lock_fname[PATH_MAX];
+ must_snprintf(lock_fname, sizeof lock_fname, "%s%s", group_name,
+ SC_NS_LOCK_FILE);
+ debug("opening lock file for namespace group %s", group_name);
+ group->lock_fd =
+ openat(group->dir_fd, lock_fname,
+ O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
+ if (group->lock_fd < 0) {
+ die("cannot open lock file for namespace group %s", group_name);
+ }
+ group->name = strdup(group_name);
+ if (group->name == NULL) {
+ die("cannot duplicate namespace group name %s", group_name);
+ }
+ return group;
+}
+
+void sc_close_ns_group(struct sc_ns_group *group)
+{
+ debug("releasing resources associated with namespace group %s",
+ group->name);
+ close(group->dir_fd);
+ close(group->lock_fd);
+ close(group->event_fd);
+ free(group->name);
+ free(group);
+}
+
+void sc_lock_ns_mutex(struct sc_ns_group *group)
+{
+ if (group->lock_fd < 0) {
+ die("precondition failed: we don't have an open file descriptor for the mutex file");
+ }
+ debug("acquiring exclusive lock for namespace group %s", group->name);
+ sc_enable_sanity_timeout();
+ if (flock(group->lock_fd, LOCK_EX) < 0) {
+ die("cannot acquire exclusive lock for namespace group %s",
+ group->name);
+ }
+ sc_disable_sanity_timeout();
+ debug("acquired exclusive lock for namespace group %s", group->name);
+}
+
+void sc_unlock_ns_mutex(struct sc_ns_group *group)
+{
+ if (group->lock_fd < 0) {
+ die("precondition failed: we don't have an open file descriptor for the mutex file");
+ }
+ debug("releasing lock for namespace group %s", group->name);
+ if (flock(group->lock_fd, LOCK_UN) < 0) {
+ die("cannot release lock for namespace group %s", group->name);
+ }
+ debug("released lock for namespace group %s", group->name);
+}
+
+void sc_create_or_join_ns_group(struct sc_ns_group *group,
+ struct sc_apparmor *apparmor)
+{
+ // Open the mount namespace file.
+ char mnt_fname[PATH_MAX];
+ must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name,
+ SC_NS_MNT_FILE);
+ int mnt_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
+ // NOTE: There is no O_EXCL here because the file can be around but
+ // doesn't have to be a mounted namespace.
+ //
+ // If the mounted namespace is discarded with
+ // sc_discard_preserved_ns_group() it will revert to a regular file. If
+ // snap-confine is killed for whatever reason after the file is created but
+ // before the file is bind-mounted it will also be a regular file.
+ mnt_fd =
+ openat(group->dir_fd, mnt_fname,
+ O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600);
+ if (mnt_fd < 0) {
+ die("cannot open mount namespace file for namespace group %s",
+ group->name);
+ }
+ // Check if we got an nsfs-based file or a regular file. This can be
+ // reliably tested because nsfs has an unique filesystem type NSFS_MAGIC.
+ // We can just ensure that this is the case thanks to fstatfs.
+ struct statfs buf;
+ if (fstatfs(mnt_fd, &buf) < 0) {
+ die("cannot perform fstatfs() on an mount namespace file descriptor");
+ }
+#ifndef NSFS_MAGIC
+// Account for kernel headers old enough to not know about NSFS_MAGIC.
+#define NSFS_MAGIC 0x6e736673
+#endif
+ if (buf.f_type == NSFS_MAGIC) {
+ char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) =
+ NULL;
+ vanilla_cwd = get_current_dir_name();
+ if (vanilla_cwd == NULL) {
+ die("cannot get the current working directory");
+ }
+ debug
+ ("attempting to re-associate the mount namespace with the namespace group %s",
+ group->name);
+ if (setns(mnt_fd, CLONE_NEWNS) < 0) {
+ die("cannot re-associate the mount namespace with namespace group %s", group->name);
+ }
+ debug
+ ("successfully re-associated the mount namespace with the namespace group %s",
+ group->name);
+ // Try to re-locate back to vanilla working directory. This can fail
+ // because that directory is no longer present.
+ if (chdir(vanilla_cwd) != 0) {
+ debug
+ ("cannot remain in %s, moving to the void directory",
+ vanilla_cwd);
+ if (chdir(SC_VOID_DIR) != 0) {
+ die("cannot change directory to %s",
+ SC_VOID_DIR);
+ }
+ debug("successfully moved to %s", SC_VOID_DIR);
+ }
+ return;
+ }
+ debug("initializing new namespace group %s", group->name);
+ // Create a new namespace and ask the caller to populate it.
+ // For rationale of forking see this:
+ // https://lists.linuxfoundation.org/pipermail/containers/2013-August/033386.html
+ //
+ // The eventfd created here is used to synchronize the child and the parent
+ // processes. It effectively tells the child to perform the capture
+ // operation.
+ group->event_fd = eventfd(0, EFD_CLOEXEC);
+ if (group->event_fd < 0) {
+ die("cannot create eventfd for mount namespace capture");
+ }
+ debug("forking support process for mount namespace capture");
+ // Store the PID of the "parent" process. This done instead of calls to
+ // getppid() because then we can reliably track the PID of the parent even
+ // if the child process is re-parented.
+ pid_t parent = getpid();
+ // Glibc defines pid as a signed 32bit integer. There's no standard way to
+ // print pid's portably so this is the best we can do.
+ pid_t pid = fork();
+ debug("forked support process has pid %d", (int)pid);
+ if (pid < 0) {
+ die("cannot fork support process for mount namespace capture");
+ }
+ if (pid == 0) {
+ // This is the child process which will capture the mount namespace.
+ //
+ // It will do so by bind-mounting the SC_NS_MNT_FILE after the parent
+ // process calls unshare() and finishes setting up the namespace
+ // completely.
+ // Change the hat to a sub-profile that has limited permissions
+ // necessary to accomplish the capture of the mount namespace.
+ debug
+ ("changing apparmor hat of the support process for mount namespace capture");
+ sc_maybe_aa_change_hat(apparmor,
+ "mount-namespace-capture-helper", 0);
+ // Configure the child to die as soon as the parent dies. In an odd
+ // case where the parent is killed then we don't want to complete our
+ // task or wait for anything.
+ if (prctl(PR_SET_PDEATHSIG, SIGINT, 0, 0, 0) < 0) {
+ die("cannot set parent process death notification signal to SIGINT");
+ }
+ // Check that parent process is still alive. If this is the case then
+ // we can *almost* reliably rely on the PR_SET_PDEATHSIG signal to wake
+ // us up from eventfd_read() below. In the rare case that the PID numbers
+ // overflow and the now-dead parent PID is recycled we will still hang
+ // forever on the read from eventfd below.
+ debug("ensuring that parent process is still alive");
+ if (kill(parent, 0) < 0) {
+ switch (errno) {
+ case ESRCH:
+ debug("parent process has already terminated");
+ abort();
+ default:
+ die("cannot ensure that parent process is still alive");
+ break;
+ }
+ }
+ if (fchdir(group->dir_fd) < 0) {
+ die("cannot move process for mount namespace capture to namespace group directory");
+ }
+ debug
+ ("waiting for a eventfd data from the parent process to continue");
+ eventfd_t value = 0;
+ sc_enable_sanity_timeout();
+ if (eventfd_read(group->event_fd, &value) < 0) {
+ die("cannot read expected data from eventfd");
+ }
+ sc_disable_sanity_timeout();
+ debug
+ ("capturing mount namespace of process %d in namespace group %s",
+ (int)parent, group->name);
+ char src[PATH_MAX];
+ char dst[PATH_MAX];
+ must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent);
+ must_snprintf(dst, sizeof dst, "%s%s", group->name,
+ SC_NS_MNT_FILE);
+ if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
+ die("cannot bind-mount the mount namespace file %s -> %s", src, dst);
+ }
+ debug
+ ("successfully captured mount namespace in namespace group %s",
+ group->name);
+ exit(0);
+ } else {
+ group->child = pid;
+ // Unshare the mount namespace and set a flag instructing the caller that
+ // the namespace is pristine and needs to be populated now.
+ debug("unsharing the mount namespace");
+ if (unshare(CLONE_NEWNS) < 0) {
+ die("cannot unshare the mount namespace");
+ }
+ group->should_populate = true;
+ }
+}
+
+bool sc_should_populate_ns_group(struct sc_ns_group *group)
+{
+ return group->should_populate;
+}
+
+void sc_preserve_populated_ns_group(struct sc_ns_group *group)
+{
+ if (group->child == 0) {
+ die("precondition failed: we don't have a support process for mount namespace capture");
+ }
+ if (group->event_fd < 0) {
+ die("precondition failed: we don't have an eventfd for mount namespace capture");
+ }
+ debug
+ ("asking support process for mount namespace capture (pid: %d) to perform the capture",
+ group->child);
+ if (eventfd_write(group->event_fd, 1) < 0) {
+ die("cannot write eventfd");
+ }
+ debug
+ ("waiting for the support process for mount namespace capture to exit");
+ int status = 0;
+ errno = 0;
+ if (waitpid(group->child, &status, 0) < 0) {
+ die("cannot wait for the support process for mount namespace capture");
+ }
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ die("support process for mount namespace capture exited abnormally");
+ }
+ debug("support process for mount namespace capture exited normally");
+ group->child = 0;
+}
+
+void sc_discard_preserved_ns_group(struct sc_ns_group *group)
+{
+ // Remember the current working directory
+ int old_dir_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
+ old_dir_fd = open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (old_dir_fd < 0) {
+ die("cannot open current directory");
+ }
+ // Move to the mount namespace directory (/run/snapd/ns)
+ if (fchdir(group->dir_fd) < 0) {
+ die("cannot move to namespace group directory");
+ }
+ // Unmount ${group_name}.mnt which holds the preserved namespace
+ char mnt_fname[PATH_MAX];
+ must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name,
+ SC_NS_MNT_FILE);
+ debug("unmounting preserved mount namespace file %s", mnt_fname);
+ if (umount2(mnt_fname, UMOUNT_NOFOLLOW) < 0) {
+ switch (errno) {
+ case EINVAL:
+ // EINVAL is returned when there's nothing to unmount (no bind-mount).
+ // Instead of checking for this explicitly (which is always racy) we
+ // just unmount and check the return code.
+ break;
+ case ENOENT:
+ // We may be asked to discard a namespace that doesn't yet
+ // exist (even the mount point may be absent). We just
+ // ignore that error and return gracefully.
+ break;
+ default:
+ die("cannot unmount preserved mount namespace file %s",
+ mnt_fname);
+ break;
+ }
+ }
+ // Get back to the original directory
+ if (fchdir(old_dir_fd) < 0) {
+ die("cannot move back to original directory");
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_NAMESPACE_SUPPORT
+#define SNAP_NAMESPACE_SUPPORT
+
+#include <stdbool.h>
+
+#include "apparmor-support.h"
+
+/**
+ * Initialize namespace sharing.
+ *
+ * This function must be called once in each process that wishes to create or
+ * join a namespace group.
+ *
+ * It is responsible for bind mounting the control directory over itself and
+ * making it private (unsharing it with all the other peers) so that it can be
+ * used for storing preserved namespaces as bind-mounted files from the nsfs
+ * filesystem (namespace filesystem).
+ *
+ * This function acquires a flock(2)-based lock to ensure that no other instance
+ * of snap-confine attempts to do this concurrently. If a process dies for any
+ * reason then the lock is released and other instances of snap-confine can
+ * complete the initialization.
+ *
+ * This function inspects /proc/self/mountinfo to determine if the directory
+ * where namespaces are kept (/run/snapd/ns) is correctly prepared as described
+ * above.
+ *
+ * For more details see namespaces(7).
+ **/
+void sc_initialize_ns_groups();
+
+/**
+ * Data required to manage namespaces amongst a group of processes.
+ */
+struct sc_ns_group;
+
+enum {
+ SC_NS_FAIL_GRACEFULLY = 1
+};
+
+/**
+ * Open a namespace group.
+ *
+ * This will open and keep file descriptors for /run/snapd/ns/ as well as for
+ * /run/snapd/ns/${group_name}.lock. The lock file is created if necessary but
+ * is not locked until sc_lock_ns_mutex() is called.
+ *
+ * If the flags argument is SC_NS_FAIL_GRACEFULLY then the function returns
+ * NULL if the /run/snapd/ns directory doesn't exist. In all other cases it
+ * calls die() and exits the process.
+ */
+struct sc_ns_group *sc_open_ns_group(const char *group_name,
+ const unsigned flags);
+
+/**
+ * Close namespace group.
+ *
+ * This will close all of the open file descriptors and release allocated memory.
+ */
+void sc_close_ns_group(struct sc_ns_group *group);
+
+/**
+ * Acquire exclusive lock to the namespace group.
+ *
+ * This will attempt to acquire an flock-based exclusive lock on the file
+ * descriptor associated with /run/snapd/ns/${group_name}.lock. If the process
+ * is killed while the lock is held the lock is automatically released by the
+ * kernel.
+ *
+ * The following methods should be called only while holding the lock:
+ * - sc_create_or_join_ns_group()
+ * - sc_should_populate_ns_group()
+ * - sc_preserve_populated_ns_group()
+ * - sc_discard_preserved_ns_group()
+ **/
+void sc_lock_ns_mutex(struct sc_ns_group *group);
+
+/**
+ * Release lock to the namespace group.
+ *
+ * This will attempt to release a flock-based lock on the file descriptor
+ * associated with /run/snapd/ns/${group_name}.lock.
+ **/
+void sc_unlock_ns_mutex(struct sc_ns_group *group);
+
+/**
+ * Join the mount namespace associated with this group if one exists.
+ *
+ * Technically the function opens /run/snapd/ns/${group_name}.mnt and tries to
+ * use setns() with the obtained file descriptor. If the call succeeds then the
+ * function returns and subsequent call to sc_should_populate_ns_group() will
+ * return false.
+ *
+ * If the call fails then an eventfd is constructed and a support process is
+ * forked. The child process waits until data is written to the eventfd (this
+ * can be done by calling sc_preserve_populated_ns_group()). In the meantime
+ * the parent process unshares the mount namespace and sets a flag so that
+ * sc_should_populate_ns_group() returns true.
+ *
+ * @returns true if the mount namespace needs to be populated
+ **/
+void sc_create_or_join_ns_group(struct sc_ns_group *group,
+ struct sc_apparmor *apparmor);
+
+/**
+ * Check if the namespace needs to be populated.
+ *
+ * If the return value is true then at this stage the namespace is already
+ * unshared. The caller should perform any mount operations that are desired
+ * and then proceed to call sc_preserve_populated_ns_group().
+ **/
+bool sc_should_populate_ns_group(struct sc_ns_group *group);
+
+/**
+ * Preserve prepared namespace group.
+ *
+ * This function signals the child support process for namespace capture to
+ * perform the capture and shut down. It must be called after the call to
+ * sc_create_or_join_ns_group() and only when sc_should_populate_ns_group()
+ * returns true.
+ *
+ * Technically this function writes to an eventfd that causes the child process
+ * to wake up, bind mount /proc/$ppid/ns/mnt to /run/snapd/ns/${group_name}.mnt
+ * and then exit. The parent process (the caller) then collects the child
+ * process and returns.
+ **/
+void sc_preserve_populated_ns_group(struct sc_ns_group *group);
+
+/**
+ * Discard the preserved namespace group.
+ *
+ * This function unmounts the bind-mounted files representing the kernel mount
+ * namespace.
+ **/
+void sc_discard_preserved_ns_group(struct sc_ns_group *group);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "config.h"
+#include "quirks.h"
+
+#include <dirent.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "utils.h"
+#include "cleanup-funcs.h"
+#include "classic.h"
+#include "mount-opt.h"
+// XXX: for smaller patch, this should be in utils.h later
+#include "user-support.h"
+
+/**
+ * Get the path to the mounted core snap in the execution environment.
+ *
+ * The core snap may be named just "core" (preferred) or "ubuntu-core"
+ * (legacy). The mount point does not depend on build-time configuration and
+ * does not differ from distribution to distribution.
+ **/
+static const char *sc_get_inner_core_mount_point()
+{
+ const char *core_path = "/snap/core/current/";
+ const char *ubuntu_core_path = "/snap/ubuntu-core/current/";
+ static const char *result = NULL;
+ if (result == NULL) {
+ if (access(core_path, F_OK) == 0) {
+ // Use the "core" snap if available.
+ result = core_path;
+ } else if (access(ubuntu_core_path, F_OK) == 0) {
+ // If not try to fall back to the "ubuntu-core" snap.
+ result = ubuntu_core_path;
+ } else {
+ die("cannot locate the core snap");
+ }
+ }
+ return result;
+}
+
+/**
+ * Mount a tmpfs at a given directory.
+ *
+ * The empty tmpfs is used as a substrate to create additional directories and
+ * then bind mounts to other destinations.
+ *
+ * It is useful to poke unexpected holes in the read-only core snap.
+ **/
+static void sc_quirk_setup_tmpfs(const char *dirname)
+{
+ debug("mounting tmpfs at %s", dirname);
+ if (mount("none", dirname, "tmpfs", MS_NODEV | MS_NOSUID, NULL) != 0) {
+ die("cannot mount tmpfs at %s", dirname);
+ };
+}
+
+/**
+ * Create an empty directory and bind mount something there.
+ *
+ * The empty directory is created at destdir. The bind mount is
+ * done from srcdir to destdir. The bind mount is performed with
+ * caller-defined flags.
+ **/
+static void sc_quirk_mkdir_bind(const char *src_dir, const char *dest_dir,
+ unsigned flags)
+{
+ flags |= MS_BIND;
+ debug("creating empty directory at %s", dest_dir);
+ if (sc_nonfatal_mkpath(dest_dir, 0755) < 0) {
+ die("cannot create empty directory at %s", dest_dir);
+ }
+ const char *flags_str = sc_mount_opt2str(flags);
+ debug("performing operation: mount %s %s -o %s", src_dir, dest_dir,
+ flags_str);
+ if (mount(src_dir, dest_dir, NULL, flags, NULL) != 0) {
+ die("cannot perform operation: mount %s %s -o %s", src_dir,
+ dest_dir, flags_str);
+ }
+}
+
+/**
+ * Create a writable mimic directory based on reference directory.
+ *
+ * The mimic directory is a tmpfs populated with bind mounts to the (possibly
+ * read only) directories in the reference directory. While all the read-only
+ * content stays read-only the actual mimic directory is writable so additional
+ * content can be placed there.
+ *
+ * Flags are forwarded to sc_quirk_mkdir_bind()
+ **/
+static void sc_quirk_create_writable_mimic(const char *mimic_dir,
+ const char *ref_dir, unsigned flags)
+{
+ debug("creating writable mimic directory %s based on %s", mimic_dir,
+ ref_dir);
+ sc_quirk_setup_tmpfs(mimic_dir);
+ debug("bind-mounting all the files from the reference directory");
+ DIR *dirp __attribute__ ((cleanup(sc_cleanup_closedir))) = NULL;
+ dirp = opendir(ref_dir);
+ if (dirp == NULL) {
+ die("cannot open reference directory %s", ref_dir);
+ }
+ struct dirent *entryp = NULL;
+ do {
+ char src_name[PATH_MAX * 2];
+ char dest_name[PATH_MAX * 2];
+ // Set errno to zero, if readdir fails it will not only return null but
+ // set errno to a non-zero value. This is how we can differentiate
+ // end-of-directory from an actual error.
+ errno = 0;
+ entryp = readdir(dirp);
+ if (entryp == NULL && errno != 0) {
+ die("cannot read another directory entry");
+ }
+ if (entryp == NULL) {
+ break;
+ }
+ if (strcmp(entryp->d_name, ".") == 0
+ || strcmp(entryp->d_name, "..") == 0) {
+ continue;
+ }
+ if (entryp->d_type != DT_DIR && entryp->d_type != DT_REG) {
+ die("unsupported entry type of file %s (%d)",
+ entryp->d_name, entryp->d_type);
+ }
+ must_snprintf(src_name, sizeof src_name, "%s/%s", ref_dir,
+ entryp->d_name);
+ must_snprintf(dest_name, sizeof dest_name, "%s/%s", mimic_dir,
+ entryp->d_name);
+ sc_quirk_mkdir_bind(src_name, dest_name, flags);
+ } while (entryp != NULL);
+}
+
+/**
+ * Setup a quirk for LXD.
+ *
+ * An existing LXD snap relies on pre-chroot behavior to access /var/lib/lxd
+ * while in devmode. Since that directory doesn't exist in the core snap the
+ * quirk punches a custom hole so that this directory shows the hostfs content
+ * if such directory exists on the host.
+ *
+ * See: https://bugs.launchpad.net/snap-confine/+bug/1613845
+ **/
+static void sc_setup_lxd_quirk()
+{
+ const char *hostfs_lxd_dir = SC_HOSTFS_DIR "/var/lib/lxd";
+ if (access(hostfs_lxd_dir, F_OK) == 0) {
+ const char *lxd_dir = "/var/lib/lxd";
+ debug("setting up quirk for LXD (see LP: #1613845)");
+ sc_quirk_mkdir_bind(hostfs_lxd_dir, lxd_dir,
+ MS_REC | MS_SLAVE | MS_NODEV | MS_NOSUID |
+ MS_NOEXEC);
+ }
+}
+
+void sc_setup_quirks()
+{
+ // because /var/lib/snapd is essential let's move it to /tmp/snapd for a sec
+ char snapd_tmp[] = "/tmp/snapd.quirks_XXXXXX";
+ if (mkdtemp(snapd_tmp) == 0) {
+ die("cannot create temporary directory for /var/lib/snapd mount point");
+ }
+ debug("performing operation: mount --move %s %s", "/var/lib/snapd",
+ snapd_tmp);
+ if (mount("/var/lib/snapd", snapd_tmp, NULL, MS_MOVE, NULL)
+ != 0) {
+ die("cannot perform operation: mount --move %s %s",
+ "/var/lib/snapd", snapd_tmp);
+ }
+ // now let's make /var/lib the vanilla /var/lib from the core snap
+ char buf[PATH_MAX];
+ must_snprintf(buf, sizeof buf, "%s/var/lib",
+ sc_get_inner_core_mount_point());
+ sc_quirk_create_writable_mimic("/var/lib", buf,
+ MS_RDONLY | MS_REC | MS_SLAVE | MS_NODEV
+ | MS_NOSUID);
+ // now let's move /var/lib/snapd (that was originally there) back
+ debug("performing operation: umount %s", "/var/lib/snapd");
+ if (umount("/var/lib/snapd") != 0) {
+ die("cannot perform operation: umount %s", "/var/lib/snapd");
+ }
+ debug("performing operation: mount --move %s %s", snapd_tmp,
+ "/var/lib/snapd");
+ if (mount(snapd_tmp, "/var/lib/snapd", NULL, MS_MOVE, NULL)
+ != 0) {
+ die("cannot perform operation: mount --move %s %s", snapd_tmp,
+ "/var/lib/snapd");
+ }
+ debug("performing operation: rmdir %s", snapd_tmp);
+ if (rmdir(snapd_tmp) != 0) {
+ die("cannot perform operation: rmdir %s", snapd_tmp);
+ }
+ // We are now ready to apply any quirks that relate to /var/lib
+ sc_setup_lxd_quirk();
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_QUIRKS_H
+#define SNAP_QUIRKS_H
+
+/**
+ * Setup various quirks that have to exists for now.
+ *
+ * This function applies non-standard tweaks that are required
+ * because of requirement to stay compatible with certain snaps
+ * that were tested with pre-chroot layout.
+ **/
+void sc_setup_quirks();
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2015-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "config.h"
+#include "seccomp-support.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <search.h>
+#include <ctype.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <sys/utsname.h>
+
+// needed for search mappings
+#include <linux/can.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sched.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <seccomp.h>
+
+#include "utils.h"
+#include "secure-getenv.h"
+
+#define sc_map_add(X) sc_map_add_kvp(#X, X)
+
+// libseccomp maximum per ARG_COUNT_MAX in src/arch.h
+#define SC_ARGS_MAXLENGTH 6
+#define SC_MAX_LINE_LENGTH 82 // 80 + '\n' + '\0'
+
+enum parse_ret {
+ PARSE_INVALID_SYSCALL = -2,
+ PARSE_ERROR = -1,
+ PARSE_OK = 0,
+};
+
+struct preprocess {
+ bool unrestricted;
+ bool complain;
+};
+
+/*
+ * arg_cmp contains items of type scmp_arg_cmp (from SCMP_CMP macro) and
+ * length is the number of items in arg_cmp that are active such that if
+ * length is '3' arg_cmp[0], arg_cmp[1] and arg_cmp[2] are used, when length
+ * is '1' only arg_cmp[0] and when length is '0', none are used.
+ */
+struct seccomp_args {
+ int syscall_nr;
+ unsigned int length;
+ struct scmp_arg_cmp arg_cmp[SC_ARGS_MAXLENGTH];
+};
+
+struct sc_map_entry {
+ ENTRY *e;
+ ENTRY *ep;
+ struct sc_map_entry *next;
+};
+
+struct sc_map_list {
+ struct sc_map_entry *list;
+ int count;
+};
+
+static char *filter_profile_dir = "/var/lib/snapd/seccomp/profiles/";
+static struct hsearch_data sc_map_htab;
+static struct sc_map_list sc_map_entries;
+
+/*
+ * Setup an hsearch map to map strings in the policy (eg, AF_UNIX) to
+ * scmp_datum_t values. Abstract away hsearch implementation behind sc_map_*
+ * functions in case we want to swap this out.
+ *
+ * sc_map_init() - initialize the hash map via linked list of
+ * of entries
+ * sc_map_add_kvp(key, value) - create entry from key/value pair and add to
+ * linked list
+ * sc_map_search(s) - if found, return scmp_datum_t for key, else set errno
+ * sc_map_destroy() - destroy the hash map and linked list
+ */
+static scmp_datum_t sc_map_search(char *s)
+{
+ ENTRY e;
+ ENTRY *ep = NULL;
+ scmp_datum_t val = 0;
+ errno = 0;
+
+ e.key = s;
+ if (hsearch_r(e, FIND, &ep, &sc_map_htab) == 0)
+ die("hsearch_r failed");
+
+ if (ep != NULL) {
+ scmp_datum_t *val_p = NULL;
+ val_p = ep->data;
+ val = *val_p;
+ } else
+ errno = EINVAL;
+
+ return val;
+}
+
+static void sc_map_add_kvp(const char *key, scmp_datum_t value)
+{
+ struct sc_map_entry *node;
+ scmp_datum_t *value_copy;
+
+ node = malloc(sizeof(*node));
+ if (node == NULL)
+ die("Out of memory creating sc_map_entries");
+
+ node->e = malloc(sizeof(*node->e));
+ if (node->e == NULL)
+ die("Out of memory creating ENTRY");
+
+ node->e->key = strdup(key);
+ if (node->e->key == NULL)
+ die("Out of memory creating e->key");
+
+ value_copy = malloc(sizeof(*value_copy));
+ if (value_copy == NULL)
+ die("Out of memory creating e->data");
+ *value_copy = value;
+ node->e->data = value_copy;
+
+ node->ep = NULL;
+ node->next = NULL;
+
+ if (sc_map_entries.list == NULL) {
+ sc_map_entries.count = 1;
+ sc_map_entries.list = node;
+ } else {
+ struct sc_map_entry *p = sc_map_entries.list;
+ while (p->next != NULL)
+ p = p->next;
+ p->next = node;
+ sc_map_entries.count++;
+ }
+}
+
+static void sc_map_init()
+{
+ // initialize the map linked list
+ sc_map_entries.list = NULL;
+ sc_map_entries.count = 0;
+
+ // build up the map linked list
+
+ // man 2 socket - domain
+ sc_map_add(AF_UNIX);
+ sc_map_add(AF_LOCAL);
+ sc_map_add(AF_INET);
+ sc_map_add(AF_INET6);
+ sc_map_add(AF_IPX);
+ sc_map_add(AF_NETLINK);
+ sc_map_add(AF_X25);
+ sc_map_add(AF_AX25);
+ sc_map_add(AF_ATMPVC);
+ sc_map_add(AF_APPLETALK);
+ sc_map_add(AF_PACKET);
+ sc_map_add(AF_ALG);
+ // linux/can.h
+ sc_map_add(AF_CAN);
+
+ // man 2 socket - type
+ sc_map_add(SOCK_STREAM);
+ sc_map_add(SOCK_DGRAM);
+ sc_map_add(SOCK_SEQPACKET);
+ sc_map_add(SOCK_RAW);
+ sc_map_add(SOCK_RDM);
+ sc_map_add(SOCK_PACKET);
+
+ // man 2 prctl
+#ifndef PR_CAP_AMBIENT
+#define PR_CAP_AMBIENT 47
+#define PR_CAP_AMBIENT_IS_SET 1
+#define PR_CAP_AMBIENT_RAISE 2
+#define PR_CAP_AMBIENT_LOWER 3
+#define PR_CAP_AMBIENT_CLEAR_ALL 4
+#endif // PR_CAP_AMBIENT
+
+ sc_map_add(PR_CAP_AMBIENT);
+ sc_map_add(PR_CAP_AMBIENT_RAISE);
+ sc_map_add(PR_CAP_AMBIENT_LOWER);
+ sc_map_add(PR_CAP_AMBIENT_IS_SET);
+ sc_map_add(PR_CAP_AMBIENT_CLEAR_ALL);
+ sc_map_add(PR_CAPBSET_READ);
+ sc_map_add(PR_CAPBSET_DROP);
+ sc_map_add(PR_SET_CHILD_SUBREAPER);
+ sc_map_add(PR_GET_CHILD_SUBREAPER);
+ sc_map_add(PR_SET_DUMPABLE);
+ sc_map_add(PR_GET_DUMPABLE);
+ sc_map_add(PR_SET_ENDIAN);
+ sc_map_add(PR_GET_ENDIAN);
+ sc_map_add(PR_SET_FPEMU);
+ sc_map_add(PR_GET_FPEMU);
+ sc_map_add(PR_SET_FPEXC);
+ sc_map_add(PR_GET_FPEXC);
+ sc_map_add(PR_SET_KEEPCAPS);
+ sc_map_add(PR_GET_KEEPCAPS);
+ sc_map_add(PR_MCE_KILL);
+ sc_map_add(PR_MCE_KILL_GET);
+ sc_map_add(PR_SET_MM);
+ sc_map_add(PR_SET_MM_START_CODE);
+ sc_map_add(PR_SET_MM_END_CODE);
+ sc_map_add(PR_SET_MM_START_DATA);
+ sc_map_add(PR_SET_MM_END_DATA);
+ sc_map_add(PR_SET_MM_START_STACK);
+ sc_map_add(PR_SET_MM_START_BRK);
+ sc_map_add(PR_SET_MM_BRK);
+ sc_map_add(PR_SET_MM_ARG_START);
+ sc_map_add(PR_SET_MM_ARG_END);
+ sc_map_add(PR_SET_MM_ENV_START);
+ sc_map_add(PR_SET_MM_ENV_END);
+ sc_map_add(PR_SET_MM_AUXV);
+ sc_map_add(PR_SET_MM_EXE_FILE);
+#ifndef PR_MPX_ENABLE_MANAGEMENT
+#define PR_MPX_ENABLE_MANAGEMENT 43
+#endif // PR_MPX_ENABLE_MANAGEMENT
+ sc_map_add(PR_MPX_ENABLE_MANAGEMENT);
+#ifndef PR_MPX_DISABLE_MANAGEMENT
+#define PR_MPX_DISABLE_MANAGEMENT 44
+#endif // PR_MPX_DISABLE_MANAGEMENT
+ sc_map_add(PR_MPX_DISABLE_MANAGEMENT);
+ sc_map_add(PR_SET_NAME);
+ sc_map_add(PR_GET_NAME);
+ sc_map_add(PR_SET_NO_NEW_PRIVS);
+ sc_map_add(PR_GET_NO_NEW_PRIVS);
+ sc_map_add(PR_SET_PDEATHSIG);
+ sc_map_add(PR_GET_PDEATHSIG);
+ sc_map_add(PR_SET_PTRACER);
+ sc_map_add(PR_SET_SECCOMP);
+ sc_map_add(PR_GET_SECCOMP);
+ sc_map_add(PR_SET_SECUREBITS);
+ sc_map_add(PR_GET_SECUREBITS);
+#ifndef PR_SET_THP_DISABLE
+#define PR_SET_THP_DISABLE 41
+#endif // PR_SET_THP_DISABLE
+ sc_map_add(PR_SET_THP_DISABLE);
+ sc_map_add(PR_TASK_PERF_EVENTS_DISABLE);
+ sc_map_add(PR_TASK_PERF_EVENTS_ENABLE);
+#ifndef PR_GET_THP_DISABLE
+#define PR_GET_THP_DISABLE 42
+#endif // PR_GET_THP_DISABLE
+ sc_map_add(PR_GET_THP_DISABLE);
+ sc_map_add(PR_GET_TID_ADDRESS);
+ sc_map_add(PR_SET_TIMERSLACK);
+ sc_map_add(PR_GET_TIMERSLACK);
+ sc_map_add(PR_SET_TIMING);
+ sc_map_add(PR_GET_TIMING);
+ sc_map_add(PR_SET_TSC);
+ sc_map_add(PR_GET_TSC);
+ sc_map_add(PR_SET_UNALIGN);
+ sc_map_add(PR_GET_UNALIGN);
+
+ // man 2 getpriority
+ sc_map_add(PRIO_PROCESS);
+ sc_map_add(PRIO_PGRP);
+ sc_map_add(PRIO_USER);
+
+ // man 2 setns
+ sc_map_add(CLONE_NEWIPC);
+ sc_map_add(CLONE_NEWNET);
+ sc_map_add(CLONE_NEWNS);
+ sc_map_add(CLONE_NEWPID);
+ sc_map_add(CLONE_NEWUSER);
+ sc_map_add(CLONE_NEWUTS);
+
+ // initialize the htab for our map
+ memset((void *)&sc_map_htab, 0, sizeof(sc_map_htab));
+ if (hcreate_r(sc_map_entries.count, &sc_map_htab) == 0)
+ die("could not create map");
+
+ // add elements from linked list to map
+ struct sc_map_entry *p = sc_map_entries.list;
+ while (p != NULL) {
+ errno = 0;
+ if (hsearch_r(*p->e, ENTER, &p->ep, &sc_map_htab) == 0)
+ die("hsearch_r failed");
+
+ if (&p->ep == NULL)
+ die("could not initialize map");
+
+ p = p->next;
+ }
+}
+
+static void sc_map_destroy()
+{
+ // this frees all of the nodes' ep so we don't have to below
+ hdestroy_r(&sc_map_htab);
+
+ struct sc_map_entry *next = sc_map_entries.list;
+ struct sc_map_entry *p = NULL;
+ while (next != NULL) {
+ p = next;
+ next = p->next;
+ free(p->e->key);
+ free(p->e->data);
+ free(p->e);
+ free(p);
+ }
+}
+
+/* Caller must check if errno != 0 */
+static scmp_datum_t read_number(char *s)
+{
+ scmp_datum_t val = 0;
+
+ errno = 0;
+
+ // per seccomp.h definition of scmp_datum_t, negative numbers are not
+ // supported, so fail if we see one or if we get one. Also fail if
+ // string is 0 length.
+ if (s[0] == '-' || s[0] == '\0') {
+ errno = EINVAL;
+ return val;
+ }
+ // check if number
+ for (int i = 0; i < strlen(s); i++) {
+ if (isdigit(s[i]) == 0) {
+ errno = EINVAL;
+ break;
+ }
+ }
+ if (errno == 0) { // found a number, so parse it
+ char *end;
+ // strtol may set errno to ERANGE
+ val = strtoul(s, &end, 10);
+ if (end == s || *end != '\0')
+ errno = EINVAL;
+ } else // try our map (sc_map_search sets errno)
+ val = sc_map_search(s);
+
+ return val;
+}
+
+static int parse_line(char *line, struct seccomp_args *sargs)
+{
+ // strtok_r needs a pointer to keep track of where it is in the
+ // string.
+ char *buf_saveptr;
+
+ // Initialize our struct
+ sargs->length = 0;
+ sargs->syscall_nr = -1;
+
+ if (strlen(line) == 0)
+ return PARSE_ERROR;
+
+ // Initialize tokenizer and obtain first token.
+ char *buf_token = strtok_r(line, " \t", &buf_saveptr);
+ if (buf_token == NULL)
+ return PARSE_ERROR;
+
+ // syscall not available on this arch/kernel
+ sargs->syscall_nr = seccomp_syscall_resolve_name(buf_token);
+ if (sargs->syscall_nr == __NR_SCMP_ERROR)
+ return PARSE_INVALID_SYSCALL;
+
+ // Parse for syscall arguments. Since we haven't yet searched for the
+ // next token, buf_token is still the syscall itself so start 'pos' as
+ // -1 and only if there is an arg to parse, increment it.
+ int pos = -1;
+ while (pos < SC_ARGS_MAXLENGTH) {
+ buf_token = strtok_r(NULL, " \t", &buf_saveptr);
+ if (buf_token == NULL)
+ break;
+ // we found a token, so increment position and process it
+ pos++;
+ if (strcmp(buf_token, "-") == 0) // skip arg
+ continue;
+
+ enum scmp_compare op = -1;
+ scmp_datum_t value = 0;
+ if (strlen(buf_token) == 0) {
+ return PARSE_ERROR;
+ } else if (strlen(buf_token) == 1) {
+ // syscall N (length of '1' indicates a single digit)
+ op = SCMP_CMP_EQ;
+ value = read_number(buf_token);
+ } else if (strncmp(buf_token, ">=", 2) == 0) {
+ // syscall >=N
+ op = SCMP_CMP_GE;
+ value = read_number(&buf_token[2]);
+ } else if (strncmp(buf_token, "<=", 2) == 0) {
+ // syscall <=N
+ op = SCMP_CMP_LE;
+ value = read_number(&buf_token[2]);
+ } else if (strncmp(buf_token, "!", 1) == 0) {
+ // syscall !N
+ op = SCMP_CMP_NE;
+ value = read_number(&buf_token[1]);
+ } else if (strncmp(buf_token, ">", 1) == 0) {
+ // syscall >N
+ op = SCMP_CMP_GT;
+ value = read_number(&buf_token[1]);
+ } else if (strncmp(buf_token, "<", 1) == 0) {
+ // syscall <N
+ op = SCMP_CMP_LT;
+ value = read_number(&buf_token[1]);
+ } else {
+ // syscall NNN
+ op = SCMP_CMP_EQ;
+ value = read_number(buf_token);
+ }
+ if (errno != 0)
+ return PARSE_ERROR;
+
+ sargs->arg_cmp[sargs->length] = SCMP_CMP(pos, op, value);
+ sargs->length++;
+
+ //printf("\nDEBUG: SCMP_CMP(%d, %d, %llu)\n", pos, op, value);
+ }
+ // too many args
+ if (pos >= SC_ARGS_MAXLENGTH)
+ return PARSE_ERROR;
+
+ return PARSE_OK;
+}
+
+// strip whitespace from the end of the given string (inplace)
+static size_t trim_right(char *s, size_t slen)
+{
+ while (slen > 0 && isspace(s[slen - 1])) {
+ s[--slen] = 0;
+ }
+ return slen;
+}
+
+// Read a relevant line and return the length. Return length '0' for comments,
+// empty lines and lines with only whitespace (so a caller can easily skip
+// them). The line buffer is right whitespaced trimmed and the final length of
+// the trimmed line is returned.
+static size_t validate_and_trim_line(char *buf, size_t buf_len, size_t lineno)
+{
+ size_t len = 0;
+
+ // comment, ignore
+ if (buf[0] == '#')
+ return len;
+
+ // ensure the entire line was read
+ len = strlen(buf);
+ if (len == 0)
+ return len;
+ else if (buf[len - 1] != '\n' && len > (buf_len - 2)) {
+ fprintf(stderr,
+ "seccomp filter line %zu was too long (%zu characters max)\n",
+ lineno, buf_len - 2);
+ errno = 0;
+ die("aborting");
+ }
+ // kill final newline
+ len = trim_right(buf, len);
+
+ return len;
+}
+
+static void preprocess_filter(FILE * f, struct preprocess *p)
+{
+ char buf[SC_MAX_LINE_LENGTH];
+ size_t lineno = 0;
+
+ p->unrestricted = false;
+ p->complain = false;
+
+ while (fgets(buf, sizeof(buf), f) != NULL) {
+ lineno++;
+
+ // skip policy-irrelevant lines
+ if (validate_and_trim_line(buf, sizeof(buf), lineno) == 0)
+ continue;
+
+ // check for special "@unrestricted" rule which short-circuits
+ // seccomp sandbox
+ if (strcmp(buf, "@unrestricted") == 0)
+ p->unrestricted = true;
+
+ // check for special "@complain" rule
+ if (strcmp(buf, "@complain") == 0)
+ p->complain = true;
+ }
+
+ if (fseek(f, 0L, SEEK_SET) != 0)
+ die("could not rewind file");
+
+ return;
+}
+
+static uint32_t uts_machine_to_seccomp_arch(const char *uts_machine)
+{
+ if (strcmp(uts_machine, "i686") == 0)
+ return SCMP_ARCH_X86;
+ else if (strcmp(uts_machine, "x86_64") == 0)
+ return SCMP_ARCH_X86_64;
+ else if (strncmp(uts_machine, "armv7", 5) == 0)
+ return SCMP_ARCH_ARM;
+#if defined (SCMP_ARCH_AARCH64)
+ else if (strncmp(uts_machine, "aarch64", 7) == 0)
+ return SCMP_ARCH_AARCH64;
+#endif
+#if defined (SCMP_ARCH_PPC64LE)
+ else if (strncmp(uts_machine, "ppc64le", 7) == 0)
+ return SCMP_ARCH_PPC64LE;
+#endif
+#if defined (SCMP_ARCH_PPC64)
+ else if (strncmp(uts_machine, "ppc64", 5) == 0)
+ return SCMP_ARCH_PPC64;
+#endif
+#if defined (SCMP_ARCH_PPC)
+ else if (strncmp(uts_machine, "ppc", 3) == 0)
+ return SCMP_ARCH_PPC;
+#endif
+#if defined (SCMP_ARCH_S390X)
+ else if (strncmp(uts_machine, "s390x", 5) == 0)
+ return SCMP_ARCH_S390X;
+#endif
+ return 0;
+}
+
+static uint32_t get_hostarch(void)
+{
+ struct utsname uts;
+ if (uname(&uts) < 0)
+ die("uname() failed");
+ uint32_t arch = uts_machine_to_seccomp_arch(uts.machine);
+ if (arch > 0)
+ return arch;
+ // Just return the seccomp userspace native arch if we can't detect the
+ // kernel host arch.
+ return seccomp_arch_native();
+}
+
+static void sc_add_seccomp_archs(scmp_filter_ctx * ctx)
+{
+ uint32_t native_arch = seccomp_arch_native(); // seccomp userspace
+ uint32_t host_arch = get_hostarch(); // kernel
+ uint32_t compat_arch = 0;
+
+ debug("host arch (kernel) is '%d'", host_arch);
+ debug("native arch (userspace) is '%d'", native_arch);
+
+ // For architectures that support a compat architecture, when the
+ // kernel and userspace match, add the compat arch, otherwise add
+ // the kernel arch to support the kernel's arch (eg, 64bit kernels with
+ // 32bit userspace).
+ if (host_arch == native_arch) {
+ switch (host_arch) {
+#if defined (SCMP_ARCH_X86_64)
+ case SCMP_ARCH_X86_64:
+ compat_arch = SCMP_ARCH_X86;
+ break;
+#endif
+#if defined(SCMP_ARCH_AARCH64)
+ case SCMP_ARCH_AARCH64:
+ compat_arch = SCMP_ARCH_ARM;
+ break;
+#endif
+#if defined (SCMP_ARCH_PPC64)
+ case SCMP_ARCH_PPC64:
+ compat_arch = SCMP_ARCH_PPC;
+ break;
+#endif
+ default:
+ break;
+ }
+ } else
+ compat_arch = host_arch;
+
+ if (compat_arch > 0 && seccomp_arch_exist(ctx, compat_arch) == -EEXIST) {
+ debug("adding compat arch '%d'", compat_arch);
+ if (seccomp_arch_add(ctx, compat_arch) < 0)
+ die("seccomp_arch_add(..., compat_arch) failed");
+ }
+}
+
+scmp_filter_ctx sc_prepare_seccomp_context(const char *filter_profile)
+{
+ int rc = 0;
+ scmp_filter_ctx ctx = NULL;
+ FILE *f = NULL;
+ size_t lineno = 0;
+ uid_t real_uid, effective_uid, saved_uid;
+ struct preprocess pre;
+ struct seccomp_args sargs;
+
+ debug("preparing seccomp profile associated with security tag %s",
+ filter_profile);
+
+ // initialize hsearch map
+ sc_map_init();
+
+ ctx = seccomp_init(SCMP_ACT_KILL);
+ if (ctx == NULL) {
+ errno = ENOMEM;
+ die("seccomp_init() failed");
+ }
+ // Setup native arch and any compatibility archs
+ sc_add_seccomp_archs(ctx);
+
+ // Disable NO_NEW_PRIVS because it interferes with exec transitions in
+ // AppArmor. Unfortunately this means that security policies must be
+ // very careful to not allow the following otherwise apps can escape
+ // the sandbox:
+ // - seccomp syscall
+ // - prctl with PR_SET_SECCOMP
+ // - ptrace (trace) in AppArmor
+ // - capability sys_admin in AppArmor
+ // Note that with NO_NEW_PRIVS disabled, CAP_SYS_ADMIN is required to
+ // change the seccomp sandbox.
+
+ if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0)
+ die("could not find user IDs");
+
+ // If running privileged or capable of raising, disable nnp
+ if (real_uid == 0 || effective_uid == 0 || saved_uid == 0)
+ if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, 0) != 0)
+ die("Cannot disable nnp");
+
+ // Note that secure_gettenv will always return NULL when suid, so
+ // SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR can't be (ab)used in that case.
+ if (secure_getenv("SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR") != NULL)
+ filter_profile_dir =
+ secure_getenv("SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR");
+
+ char profile_path[512]; // arbitrary path name limit
+ must_snprintf(profile_path, sizeof(profile_path), "%s/%s",
+ filter_profile_dir, filter_profile);
+
+ f = fopen(profile_path, "r");
+ if (f == NULL) {
+ fprintf(stderr, "Can not open %s (%s)\n", profile_path,
+ strerror(errno));
+ die("aborting");
+ }
+ // Note, preprocess_filter() die()s on error
+ preprocess_filter(f, &pre);
+
+ if (pre.unrestricted) {
+ seccomp_release(ctx);
+ ctx = NULL;
+ goto out;
+ }
+ // FIXME: right now complain mode is the equivalent to unrestricted.
+ // We'll want to change this once we seccomp logging is in order.
+ if (pre.complain) {
+ seccomp_release(ctx);
+ ctx = NULL;
+ goto out;
+ }
+
+ char buf[SC_MAX_LINE_LENGTH];
+ while (fgets(buf, sizeof(buf), f) != NULL) {
+ lineno++;
+
+ // skip policy-irrelevant lines
+ if (validate_and_trim_line(buf, sizeof(buf), lineno) == 0)
+ continue;
+
+ char *buf_copy = strdup(buf);
+ if (buf_copy == NULL)
+ die("Out of memory");
+
+ int pr_rc = parse_line(buf_copy, &sargs);
+ free(buf_copy);
+ if (pr_rc != PARSE_OK) {
+ // as this is a syscall whitelist an invalid syscall
+ // is ok and the error can be ignored
+ if (pr_rc == PARSE_INVALID_SYSCALL)
+ continue;
+ die("could not parse line");
+ }
+
+ rc = seccomp_rule_add_exact_array(ctx, SCMP_ACT_ALLOW,
+ sargs.syscall_nr,
+ sargs.length, sargs.arg_cmp);
+ if (rc != 0) {
+ rc = seccomp_rule_add_array(ctx, SCMP_ACT_ALLOW,
+ sargs.syscall_nr,
+ sargs.length,
+ sargs.arg_cmp);
+ if (rc != 0) {
+ fprintf(stderr,
+ "seccomp_rule_add_array failed with %i for '%s'\n",
+ rc, buf);
+ errno = 0;
+ die("aborting");
+ }
+ }
+ }
+
+ out:
+ if (f != NULL) {
+ if (fclose(f) != 0)
+ die("could not close seccomp file");
+ }
+ sc_map_destroy();
+ return ctx;
+}
+
+void sc_load_seccomp_context(scmp_filter_ctx ctx)
+{
+ int rc;
+ uid_t real_uid, effective_uid, saved_uid;
+
+ // if sc_prepare_seccomp_context() sees @unrestricted or @complain it bails
+ // out early and destroys the context object. In that case we have nothing
+ // to do.
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0)
+ die("could not find user IDs");
+
+ // If not root but can raise, then raise privileges to load seccomp
+ // policy since we don't have nnp
+ debug("raising privileges to load seccomp profile");
+ if (effective_uid != 0 && saved_uid == 0) {
+ if (seteuid(0) != 0)
+ die("seteuid failed");
+ if (geteuid() != 0)
+ die("raising privs before seccomp_load did not work");
+ }
+ // load it into the kernel
+ debug("loading seccomp profile into the kernel");
+ rc = seccomp_load(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "seccomp_load failed with %i\n", rc);
+ die("aborting");
+ }
+ // drop privileges again
+ debug("dropping privileges after loading seccomp profile");
+ if (geteuid() == 0) {
+ unsigned real_uid = getuid();
+ if (seteuid(real_uid) != 0)
+ die("seteuid failed");
+ if (real_uid != 0 && geteuid() == 0)
+ die("dropping privs after seccomp_load did not work");
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2015-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef SNAP_CONFINE_SECCOMP_SUPPORT_H
+#define SNAP_CONFINE_SECCOMP_SUPPORT_H
+
+#include <seccomp.h>
+
+/**
+ * Prepare seccomp profile associated with the security tag.
+ *
+ * This function loads the seccomp profile from
+ * /var/lib/snapd/seccomp/profiles/$SECURITY_TAG and stores it into
+ * scmp_filter_ctx object.
+ *
+ * The object is returned to the caller and can be made effective with a call
+ * to sc_load_seccomp_context(). The returned value should be cleaned up with
+ * seccomp_release().
+ *
+ * This function calls die() on all errors.
+ **/
+
+scmp_filter_ctx sc_prepare_seccomp_context(const char *security_tag);
+
+/**
+ * Load a seccomp context.
+ *
+ * This function calls seccomp_load(3) and handles errors if it fails.
+ **/
+void sc_load_seccomp_context(scmp_filter_ctx ctx);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "secure-getenv.h"
+
+#include <stdlib.h>
+#include <sys/auxv.h>
+
+#ifndef HAVE_SECURE_GETENV
+char *secure_getenv(const char *name)
+{
+ unsigned long secure = getauxval(AT_SECURE);
+ if (secure != 0) {
+ return NULL;
+ }
+ return getenv(name);
+}
+#endif // ! HAVE_SECURE_GETENV
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef SNAP_CONFINE_SECURE_GETENV_H
+#define SNAP_CONFINE_SECURE_GETENV_H
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifndef HAVE_SECURE_GETENV
+/**
+ * Secure version of getenv()
+ *
+ * This version returns NULL if the process is running within a secure context.
+ * This is exactly the same as the GNU extension to the standard library. It is
+ * only used when glibc is not available.
+ **/
+char *secure_getenv(const char *name)
+ __attribute__ ((nonnull(1), warn_unused_result));
+#endif // ! HAVE_SECURE_GETENV
+
+#endif
--- /dev/null
+# Author: Jamie Strandboge <jamie@canonical.com>
+#include <tunables/global>
+
+@LIBEXECDIR@/snap-confine (attach_disconnected) {
+ # We run privileged, so be fanatical about what we include and don't use
+ # any abstractions
+ /etc/ld.so.cache r,
+ /lib/@{multiarch}/ld-*.so mr,
+ # libc, you are funny
+ /lib/@{multiarch}/libc{,-[0-9]*}.so* mr,
+ /lib/@{multiarch}/libpthread{,-[0-9]*}.so* mr,
+ /lib/@{multiarch}/librt{,-[0-9]*}.so* mr,
+ /lib/@{multiarch}/libgcc_s.so* mr,
+ # normal libs in order
+ /lib/@{multiarch}/libapparmor.so* mr,
+ /lib/@{multiarch}/libcgmanager.so* mr,
+ /lib/@{multiarch}/libdl-[0-9]*.so* mr,
+ /lib/@{multiarch}/libnih.so* mr,
+ /lib/@{multiarch}/libnih-dbus.so* mr,
+ /lib/@{multiarch}/libdbus-1.so* mr,
+ /lib/@{multiarch}/libudev.so* mr,
+ /usr/lib/@{multiarch}/libseccomp.so* mr,
+ /lib/@{multiarch}/libseccomp.so* mr,
+
+ @LIBEXECDIR@/snap-confine mr,
+
+ /dev/null rw,
+ /dev/full rw,
+ /dev/zero rw,
+ /dev/random r,
+ /dev/urandom r,
+ /dev/pts/[0-9]* rw,
+
+ # cgroups
+ capability sys_admin,
+ capability dac_override,
+ /sys/fs/cgroup/devices/snap{,py}.*/ w,
+ /sys/fs/cgroup/devices/snap{,py}.*/tasks w,
+ /sys/fs/cgroup/devices/snap{,py}.*/devices.{allow,deny} w,
+
+ # querying udev
+ /etc/udev/udev.conf r,
+ /sys/devices/**/uevent r,
+ /lib/udev/snappy-app-dev ixr, # drop
+ /run/udev/** rw,
+ /{,usr/}bin/tr ixr,
+ /usr/lib/locale/** r,
+ /usr/lib/@{multiarch}/gconv/gconv-modules r,
+ /usr/lib/@{multiarch}/gconv/gconv-modules.cache r,
+
+ # priv dropping
+ capability setuid,
+ capability setgid,
+
+ # changing profile
+ @{PROC}/[0-9]*/attr/exec w,
+ # Reading current profile
+ @{PROC}/[0-9]*/attr/current r,
+
+ # To find where apparmor is mounted
+ @{PROC}/[0-9]*/mounts r,
+ # To find if apparmor is enabled
+ /sys/module/apparmor/parameters/enabled r,
+
+ # Don't allow changing profile to unconfined or profiles that start with
+ # '/'. Use 'unsafe' to support snap-exec on armhf and its reliance on
+ # the environment for determining the capabilities of the architecture.
+ # 'unsafe' is ok here because the kernel will have already cleared the
+ # environment as part of launching snap-confine with
+ # CAP_SYS_ADMIN.
+ change_profile unsafe /** -> [^u/]**,
+ change_profile unsafe /** -> u[^n]**,
+ change_profile unsafe /** -> un[^c]**,
+ change_profile unsafe /** -> unc[^o]**,
+ change_profile unsafe /** -> unco[^n]**,
+ change_profile unsafe /** -> uncon[^f]**,
+ change_profile unsafe /** -> unconf[^i]**,
+ change_profile unsafe /** -> unconfi[^n]**,
+ change_profile unsafe /** -> unconfin[^e]**,
+ change_profile unsafe /** -> unconfine[^d]**,
+ change_profile unsafe /** -> unconfined?**,
+
+ # allow changing to a few not caught above
+ change_profile unsafe /** -> {u,un,unc,unco,uncon,unconf,unconfi,unconfin,unconfine},
+
+ # LP: #1446794 - when this bug is fixed, change the above to:
+ # deny change_profile unsafe /** -> {unconfined,/**},
+ # change_profile unsafe /** -> **,
+
+ # reading seccomp filters
+ /{tmp/snap.rootfs_*/,}var/lib/snapd/seccomp/profiles/* r,
+
+ # reading mount profiles
+ /{tmp/snap.rootfs_*/,}var/lib/snapd/mount/*.fstab r,
+
+ # boostrapping the mount namespace
+ mount options=(rw rshared) -> /,
+ mount options=(rw bind) /tmp/snap.rootfs_*/ -> /tmp/snap.rootfs_*/,
+ mount options=(rw unbindable) -> /tmp/snap.rootfs_*/,
+ # the next line is for classic system
+ mount options=(rw rbind) @SNAP_MOUNT_DIR@/{,ubuntu-}core/*/ -> /tmp/snap.rootfs_*/,
+ # the next line is for core system
+ mount options=(rw rbind) / -> /tmp/snap.rootfs_*/,
+ # all of the constructed rootfs is a rslave
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/,
+ # bidirectional mounts (for both classic and core)
+ # NOTE: this doesn't capture the MERGED_USR configuration option so that
+ # when a distro with merged /usr and / that uses apparmor shows up it
+ # should be handled here.
+ /{,run/}media/ w,
+ mount options=(rw rbind) /media/ -> /tmp/snap.rootfs_*/media/,
+ /run/netns/ w,
+ mount options=(rw rbind) /run/netns/ -> /tmp/snap.rootfs_*/run/netns/,
+ # unidirectional mounts (only for classic system)
+ mount options=(rw rbind) /dev/ -> /tmp/snap.rootfs_*/dev/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/dev/,
+
+ mount options=(rw rbind) /etc/ -> /tmp/snap.rootfs_*/etc/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/etc/,
+
+ mount options=(rw rbind) /home/ -> /tmp/snap.rootfs_*/home/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/home/,
+
+ mount options=(rw rbind) /root/ -> /tmp/snap.rootfs_*/root/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/root/,
+
+ mount options=(rw rbind) /proc/ -> /tmp/snap.rootfs_*/proc/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/proc/,
+
+ mount options=(rw rbind) /sys/ -> /tmp/snap.rootfs_*/sys/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/sys/,
+
+ mount options=(rw rbind) /tmp/ -> /tmp/snap.rootfs_*/tmp/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/tmp/,
+
+ mount options=(rw rbind) /var/lib/snapd/ -> /tmp/snap.rootfs_*/var/lib/snapd/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/lib/snapd/,
+
+ mount options=(rw rbind) /var/snap/ -> /tmp/snap.rootfs_*/var/snap/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/snap/,
+
+ mount options=(rw rbind) /var/tmp/ -> /tmp/snap.rootfs_*/var/tmp/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/tmp/,
+
+ mount options=(rw rbind) /run/ -> /tmp/snap.rootfs_*/run/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/run/,
+
+ mount options=(rw rbind) {/usr,}/lib/modules/ -> /tmp/snap.rootfs_*/lib/modules/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/lib/modules/,
+
+ mount options=(rw rbind) /var/log/ -> /tmp/snap.rootfs_*/var/log/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/log/,
+
+ mount options=(rw rbind) /usr/src/ -> /tmp/snap.rootfs_*/usr/src/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/usr/src/,
+ # /etc/alternatives (classic)
+ mount options=(rw bind) @SNAP_MOUNT_DIR@/{,ubuntu-}core/*/etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/,
+ # /etc/alternatives (core)
+ mount options=(rw bind) /etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/,
+ mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/alternatives/,
+ # the /snap directory
+ mount options=(rw rbind) @SNAP_MOUNT_DIR@/ -> /tmp/snap.rootfs_*/snap/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*/snap/,
+ # pivot_root preparation and execution
+ mount options=(rw bind) /tmp/snap.rootfs_*/var/lib/snapd/hostfs/ -> /tmp/snap.rootfs_*/var/lib/snapd/hostfs/,
+ mount options=(rw private) -> /tmp/snap.rootfs_*/var/lib/snapd/hostfs/,
+ pivot_root,
+ # cleanup
+ umount /var/lib/snapd/hostfs/tmp/snap.rootfs_*/,
+ umount /var/lib/snapd/hostfs/sys/,
+ umount /var/lib/snapd/hostfs/dev/,
+ umount /var/lib/snapd/hostfs/proc/,
+ mount options=(rw rslave) -> /var/lib/snapd/hostfs/,
+
+ # set up snap-specific private /tmp dir
+ capability chown,
+ /tmp/ w,
+ /tmp/snap.*/ w,
+ /tmp/snap.*/tmp/ w,
+ mount options=(rw private) -> /tmp/,
+ mount options=(rw bind) /tmp/snap.*/tmp/ -> /tmp/,
+ mount fstype=devpts options=(rw) devpts -> /dev/pts/,
+ mount options=(rw bind) /dev/pts/ptmx -> /dev/ptmx, # for bind mounting
+ mount options=(rw bind) /dev/pts/ptmx -> /dev/pts/ptmx, # for bind mounting under LXD
+ # Workaround for LP: #1584456 on older kernels that mistakenly think
+ # /dev/pts/ptmx needs a trailing '/'
+ mount options=(rw bind) /dev/pts/ptmx/ -> /dev/ptmx/,
+ mount options=(rw bind) /dev/pts/ptmx/ -> /dev/pts/ptmx/,
+
+ # for running snaps on classic
+ /snap/ r,
+ /snap/** r,
+ @SNAP_MOUNT_DIR@/ r,
+ @SNAP_MOUNT_DIR@/** r,
+
+ # NOTE: at this stage the /snap directory is stable as we have called
+ # pivot_root already.
+
+ # Support mount profiles via the content interface. This should correspond
+ # to permutations of $SNAP -> $SNAP for reading and $SNAP_{DATA,COMMON} ->
+ # $SNAP_{DATA,COMMON} for both reading and writing.
+ #
+ # Note that:
+ # /snap/*/*/**
+ # is meant to mean:
+ # /snap/$SNAP_NAME/$SNAP_REVISION/and-any-subdirectory
+ # but:
+ # /var/snap/*/**
+ # is meant to mean:
+ # /var/snap/$SNAP_NAME/$SNAP_REVISION/
+ mount options=(ro bind) /snap/*/** -> /snap/*/*/**,
+ mount options=(ro bind) /snap/*/** -> /var/snap/*/**,
+ mount options=(rw bind) /var/snap/*/** -> /var/snap/*/**,
+ mount options=(ro bind) /var/snap/*/** -> /var/snap/*/**,
+ # But we don't want anyone to touch /snap/bin
+ audit deny mount /snap/bin/** -> /**,
+ audit deny mount /** -> /snap/bin/**,
+ # Allow the content interface to bind fonts from the host filesystem
+ mount options=(ro bind) /var/lib/snapd/hostfs/usr/share/fonts/ -> /snap/*/*/**,
+
+ # nvidia handling, glob needs /usr/** and the launcher must be
+ # able to bind mount the nvidia dir
+ /sys/module/nvidia/version r,
+ /usr/** r,
+ mount options=(rw bind) /usr/lib/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl/,
+
+ # for chroot on steroids, we use pivot_root as a better chroot that makes
+ # apparmor rules behave the same on classic and outside of classic.
+
+ # for creating the user data directories: ~/snap, ~/snap/<name> and
+ # ~/snap/<name>/<version>
+ / r,
+ @{HOMEDIRS}/ r,
+ # These should both have 'owner' match but due to LP: #1466234, we can't
+ # yet
+ @{HOME}/ r,
+ @{HOME}/snap/{,*/,*/*/} rw,
+
+ # for creating the user shared memory directories
+ /{dev,run}/{,shm/} r,
+ # This should both have 'owner' match but due to LP: #1466234, we can't yet
+ /{dev,run}/shm/{,*/,*/*/} rw,
+
+ # for creating the user XDG_RUNTIME_DIR: /run/user, /run/user/UID and
+ # /run/user/UID/<name>
+ /run/user/{,[0-9]*/,[0-9]*/*/} rw,
+
+ # Workaround https://launchpad.net/bugs/359338 until upstream handles
+ # stacked filesystems generally.
+ # encrypted ~/.Private and old-style encrypted $HOME
+ @{HOME}/.Private/ r,
+ @{HOME}/.Private/** mrixwlk,
+ # new-style encrypted $HOME
+ @{HOMEDIRS}/.ecryptfs/*/.Private/ r,
+ @{HOMEDIRS}/.ecryptfs/*/.Private/** mrixwlk,
+
+ # Allow snap-confine to move to the void
+ /var/lib/snapd/void/ r,
+
+ # Support for the quirk system
+ /var/ r,
+ /var/lib/ r,
+ /var/lib/** rw,
+ /tmp/ r,
+ /tmp/snapd.quirks_*/ rw,
+ mount options=(move) /var/lib/snapd/ -> /tmp/snapd.quirks_*/,
+ mount fstype=tmpfs options=(rw nodev nosuid) none -> /var/lib/,
+ mount options=(ro rbind) /snap/{,ubuntu-}core/*/var/lib/** -> /var/lib/**,
+ umount /var/lib/snapd/,
+ mount options=(move) /tmp/snapd.quirks_*/ -> /var/lib/snapd/,
+
+ # support for the LXD quirk
+ mount options=(rw rbind nodev nosuid noexec) /var/lib/snapd/hostfs/var/lib/lxd/ -> /var/lib/lxd/,
+ /var/lib/lxd/ w,
+ /var/lib/snapd/hostfs/var/lib/lxd r,
+
+ # support for the mount namespace sharing
+ mount options=(rw rbind) /run/snapd/ns/ -> /run/snapd/ns/,
+ mount options=(private) -> /run/snapd/ns/,
+ / rw,
+ /run/ rw,
+ /run/snapd/ rw,
+ /run/snapd/ns/ rw,
+ /run/snapd/ns/*.lock rwk,
+ /run/snapd/ns/*.mnt rw,
+ ptrace (read, readby, tracedby) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper,
+ @{PROC}/*/mountinfo r,
+ capability sys_chroot,
+ capability sys_admin,
+ signal (send, receive) set=(abrt) peer=@LIBEXECDIR@/snap-confine,
+ signal (send) set=(int) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper,
+ signal (send, receive) set=(alrm, exists) peer=@LIBEXECDIR@/snap-confine,
+ signal (receive) set=(exists) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper,
+
+ # For aa_change_hat() to go into ^mount-namespace-capture-helper
+ @{PROC}/[0-9]*/attr/current w,
+
+ ^mount-namespace-capture-helper (attach_disconnected) {
+ # We run privileged, so be fanatical about what we include and don't use
+ # any abstractions
+ /etc/ld.so.cache r,
+ /lib/@{multiarch}/ld-*.so mr,
+ # libc, you are funny
+ /lib/@{multiarch}/libc{,-[0-9]*}.so* mr,
+ /lib/@{multiarch}/libpthread{,-[0-9]*}.so* mr,
+ /lib/@{multiarch}/librt{,-[0-9]*}.so* mr,
+ /lib/@{multiarch}/libgcc_s.so* mr,
+ # normal libs in order
+ /lib/@{multiarch}/libapparmor.so* mr,
+ /lib/@{multiarch}/libcgmanager.so* mr,
+ /lib/@{multiarch}/libnih.so* mr,
+ /lib/@{multiarch}/libnih-dbus.so* mr,
+ /lib/@{multiarch}/libdbus-1.so* mr,
+ /lib/@{multiarch}/libudev.so* mr,
+ /usr/lib/@{multiarch}/libseccomp.so* mr,
+ /lib/@{multiarch}/libseccomp.so* mr,
+
+ @LIBEXECDIR@/snap-confine mr,
+
+ /dev/null rw,
+ /dev/full rw,
+ /dev/zero rw,
+ /dev/random r,
+ /dev/urandom r,
+
+ capability sys_ptrace,
+ capability sys_admin,
+ # This allows us to read and bind mount the namespace file
+ / r,
+ @{PROC}/ r,
+ @{PROC}/*/ r,
+ @{PROC}/*/ns/ r,
+ @{PROC}/*/ns/mnt r,
+ /run/ r,
+ /run/snapd/ r,
+ /run/snapd/ns/ r,
+ /run/snapd/ns/*.mnt rw,
+ # NOTE: the source name is / even though we map /proc/123/ns/mnt
+ mount options=(rw bind) / -> /run/snapd/ns/*.mnt,
+ # This is the SIGALRM that we send and receive if a timeout expires
+ signal (send, receive) set=(alrm) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper,
+ # Those two rules are exactly the same but we don't know if the parent process is still alive
+ # and hence has the appropriate label or is already dead and hence has no label.
+ signal (send) set=(exists) peer=@LIBEXECDIR@/snap-confine,
+ signal (send) set=(exists) peer=unconfined,
+ # This is so that we can abort
+ signal (send, receive) set=(abrt) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper,
+ # This is the signal we get if snap-confine dies (we subscribe to it with prctl)
+ signal (receive) set=(int) peer=@LIBEXECDIR@/snap-confine,
+ # This allows snap-confine to be killed from the outside.
+ signal (receive) peer=unconfined,
+ # This allows snap-confine to wait for us
+ ptrace (read, trace, tracedby) peer=@LIBEXECDIR@/snap-confine,
+ }
+
+ # Allow snap-confine to be killed
+ signal (receive) peer=unconfined,
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "classic.h"
+#include "mount-support.h"
+#include "snap.h"
+#include "utils.h"
+#ifdef HAVE_SECCOMP
+#include "seccomp-support.h"
+#endif // ifdef HAVE_SECCOMP
+#include "udev-support.h"
+#include "cleanup-funcs.h"
+#include "user-support.h"
+#include "ns-support.h"
+#include "quirks.h"
+#include "secure-getenv.h"
+#include "apparmor-support.h"
+
+int main(int argc, char **argv)
+{
+ if (argc == 2 && strcmp(argv[1], "--version") == 0) {
+ printf("%s %s\n", PACKAGE, PACKAGE_VERSION);
+ return 0;
+ }
+ char *basename = strrchr(argv[0], '/');
+ if (basename) {
+ debug("setting argv[0] to %s", basename + 1);
+ argv[0] = basename + 1;
+ }
+ if (argc > 1 && !strcmp(argv[0], "ubuntu-core-launcher")) {
+ debug("shifting arguments by one");
+ argv[1] = argv[0];
+ argv++;
+ argc--;
+ }
+ // XXX: replace with pull request 2416 in snapd
+ bool classic_confinement = false;
+ if (argc > 2 && strcmp(argv[1], "--classic") == 0) {
+ classic_confinement = true;
+ // Shift remaining arguments left
+ int i;
+ for (i = 1; i + 1 < argc; ++i) {
+ argv[i] = argv[i + 1];
+ }
+ argv[i] = NULL;
+ argc -= 1;
+ }
+ const int NR_ARGS = 2;
+ if (argc < NR_ARGS + 1)
+ die("Usage: %s <security-tag> <binary>", argv[0]);
+
+ const char *security_tag = argv[1];
+ debug("security tag is %s", security_tag);
+ const char *binary = argv[2];
+ debug("binary to run is %s", binary);
+ uid_t real_uid = getuid();
+ gid_t real_gid = getgid();
+
+ if (!verify_security_tag(security_tag))
+ die("security tag %s not allowed", security_tag);
+
+#ifndef CAPS_OVER_SETUID
+ // this code always needs to run as root for the cgroup/udev setup,
+ // however for the tests we allow it to run as non-root
+ if (geteuid() != 0 && secure_getenv("SNAP_CONFINE_NO_ROOT") == NULL) {
+ die("need to run as root or suid");
+ }
+#endif
+ struct sc_apparmor apparmor;
+ sc_init_apparmor_support(&apparmor);
+#ifdef HAVE_SECCOMP
+ scmp_filter_ctx seccomp_ctx
+ __attribute__ ((cleanup(sc_cleanup_seccomp_release))) = NULL;
+ seccomp_ctx = sc_prepare_seccomp_context(security_tag);
+#endif // ifdef HAVE_SECCOMP
+
+ if (geteuid() == 0) {
+ if (classic_confinement) {
+ /* 'classic confinement' is designed to run without the sandbox
+ * inside the shared namespace. Specifically:
+ * - snap-confine skips using the snap-specific mount namespace
+ * - snap-confine skips using device cgroups
+ * - snapd sets up a lenient AppArmor profile for snap-confine to use
+ * - snapd sets up a lenient seccomp profile for snap-confine to use
+ */
+ debug
+ ("skipping sandbox setup, classic confinement in use");
+ } else {
+ const char *group_name = getenv("SNAP_NAME");
+ if (group_name == NULL) {
+ die("SNAP_NAME is not set");
+ }
+ sc_initialize_ns_groups();
+ struct sc_ns_group *group = NULL;
+ group = sc_open_ns_group(group_name, 0);
+ sc_lock_ns_mutex(group);
+ sc_create_or_join_ns_group(group, &apparmor);
+ if (sc_should_populate_ns_group(group)) {
+ sc_populate_mount_ns(security_tag);
+ sc_preserve_populated_ns_group(group);
+ }
+ sc_unlock_ns_mutex(group);
+ sc_close_ns_group(group);
+ // Reset path as we cannot rely on the path from the host OS to
+ // make sense. The classic distribution may use any PATH that makes
+ // sense but we cannot assume it makes sense for the core snap
+ // layout. Note that the /usr/local directories are explicitly
+ // left out as they are not part of the core snap.
+ debug
+ ("resetting PATH to values in sync with core snap");
+ setenv("PATH",
+ "/usr/sbin:/usr/bin:/sbin:/bin:/usr/games", 1);
+ struct snappy_udev udev_s;
+ if (snappy_udev_init(security_tag, &udev_s) == 0)
+ setup_devices_cgroup(security_tag, &udev_s);
+ snappy_udev_cleanup(&udev_s);
+ }
+ // The rest does not so temporarily drop privs back to calling
+ // user (we'll permanently drop after loading seccomp)
+ if (setegid(real_gid) != 0)
+ die("setegid failed");
+ if (seteuid(real_uid) != 0)
+ die("seteuid failed");
+
+ if (real_gid != 0 && geteuid() == 0)
+ die("dropping privs did not work");
+ if (real_uid != 0 && getegid() == 0)
+ die("dropping privs did not work");
+ }
+ // Ensure that the user data path exists.
+ setup_user_data();
+#if 0
+ setup_user_xdg_runtime_dir();
+#endif
+
+ // https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement
+ sc_maybe_aa_change_onexec(&apparmor, security_tag);
+#ifdef HAVE_SECCOMP
+ sc_load_seccomp_context(seccomp_ctx);
+#endif // ifdef HAVE_SECCOMP
+
+ // Permanently drop if not root
+ if (geteuid() == 0) {
+ // Note that we do not call setgroups() here because its ok
+ // that the user keeps the groups he already belongs to
+ if (setgid(real_gid) != 0)
+ die("setgid failed");
+ if (setuid(real_uid) != 0)
+ die("setuid failed");
+
+ if (real_gid != 0 && (getuid() == 0 || geteuid() == 0))
+ die("permanently dropping privs did not work");
+ if (real_uid != 0 && (getgid() == 0 || getegid() == 0))
+ die("permanently dropping privs did not work");
+ }
+ // and exec the new binary
+ execv(binary, (char *const *)&argv[NR_ARGS]);
+ perror("execv failed");
+ return 1;
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "utils.h"
+#include "ns-support.h"
+
+int main(int argc, char **argv)
+{
+ if (argc != 2)
+ die("Usage: %s snap-name", argv[0]);
+ const char *snap_name = argv[1];
+ struct sc_ns_group *group =
+ sc_open_ns_group(snap_name, SC_NS_FAIL_GRACEFULLY);
+ if (group != NULL) {
+ sc_lock_ns_mutex(group);
+ sc_discard_preserved_ns_group(group);
+ sc_unlock_ns_mutex(group);
+ sc_close_ns_group(group);
+ }
+ return 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "config.h"
+#include "snap.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <regex.h>
+
+#include "utils.h"
+
+bool verify_security_tag(const char *security_tag)
+{
+ // The executable name is of form:
+ // snap.<name>.(<appname>|hook.<hookname>)
+ // - <name> must start with lowercase letter, then may contain
+ // lowercase alphanumerics and '-'
+ // - <appname> may contain alphanumerics and '-'
+ // - <hookname must start with a lowercase letter, then may
+ // contain lowercase letters and '-'
+ const char *whitelist_re =
+ "^snap\\.[a-z](-?[a-z0-9])*\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$";
+ regex_t re;
+ if (regcomp(&re, whitelist_re, REG_EXTENDED | REG_NOSUB) != 0)
+ die("can not compile regex %s", whitelist_re);
+
+ int status = regexec(&re, security_tag, 0, NULL, 0);
+ regfree(&re);
+
+ return (status == 0);
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_SNAP_H
+#define SNAP_CONFINE_SNAP_H
+
+#include <stdbool.h>
+
+bool verify_security_tag(const char *security_tag);
+
+#endif
--- /dev/null
+#!/bin/sh
+# udev callout (should live in /lib/udev) to allow a snap to access a device node
+set -e
+# debugging
+#exec >>/tmp/snappy-app-dev.log
+#exec 2>&1
+#set -x
+# end debugging
+
+ACTION="$1"
+APPNAME="$2"
+DEVPATH="$3"
+MAJMIN="$4"
+[ -n "$APPNAME" ] || { echo "no app name given" >&2; exit 1; }
+[ -n "$DEVPATH" ] || { echo "no devpath given" >&2; exit 1; }
+[ -n "$MAJMIN" ] || { echo "no major/minor given" >&2; exit 0; }
+
+APPNAME=`echo $APPNAME | tr '_' '.'`
+app_dev_cgroup="/sys/fs/cgroup/devices/$APPNAME"
+
+# check if it's a block or char dev
+if [ "${DEVPATH#*/block/}" != "$DEVPATH" ]; then
+ type="b"
+else
+ type="c"
+fi
+
+acl="$type $MAJMIN rwm"
+case "$ACTION" in
+ add|change)
+ echo "$acl" > "$app_dev_cgroup/devices.allow"
+ ;;
+ remove)
+ echo "$acl" > "$app_dev_cgroup/devices.deny"
+ ;;
+ *)
+ echo "ERROR: unknown action $ACTION" >&2
+ exit 1 ;;
+esac
--- /dev/null
+This directory contains keys used by the sbuild program to sign the temporary
+archive. Those keys are kept in the tree as ephemeral test virtual machines do
+not have sufficient entropy to generate keys by themselves in reasonable amount
+of time.
--- /dev/null
+distro_codename=sid
+distro_packaging_git_branch=debian
--- /dev/null
+if [ -n "${APT_PROXY:-}" ]; then
+ distro_archive=${APT_PROXY}/ftp.debian.org/debian
+else
+ distro_archive=http://ftp.debian.org/debian
+fi
+# NOTE: Debian packaging needs to be updated. I sent a mail to the
+# debian maintainer with instructions on what needs to happen and
+# how it fits into the CI system.
+#
+# For now all builds on debian will fail as they still contains
+# debian/patches that are now applied upstream.
+distro_packaging_git=git://anonscm.debian.org/collab-maint/snap-confine.git
--- /dev/null
+distro_codename=trusty
+distro_packaging_git_branch=14.04
--- /dev/null
+distro_codename=xenial
+distro_packaging_git_branch=16.04
--- /dev/null
+distro_codename=yakkety
+distro_packaging_git_branch=16.10
--- /dev/null
+if [ -n "${APT_PROXY:-}" ]; then
+ distro_archive=${APT_PROXY}/archive.ubuntu.com/ubuntu
+else
+ distro_archive=http://archive.ubuntu.com/ubuntu
+fi
+distro_packaging_git=https://git.launchpad.net/snap-confine
+sbuild_createchroot_extra="--components=main,universe"
--- /dev/null
+summary: Check that launcher cgroup functionality works
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ echo "Install snapd-hacker-toolbelt"
+ snap install snapd-hacker-toolbelt
+execute: |
+ cd /
+ echo "Clear udev tags and cgroups with non-test device and running snapd-hacker-toolbelt.busybox"
+ echo 'KERNEL=="uinput", TAG+="snap_snapd-hacker-toolbelt_busybox"' > /etc/udev/rules.d/70-spread-test.rules
+ udevadm control --reload-rules
+ udevadm settle
+ udevadm trigger
+ udevadm settle
+ snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello
+ echo "Verify no tags for snapd-hacker-toolbelt.busybox for kmsg"
+ if udevadm info /sys/devices/virtual/mem/kmsg | grep snap_snapd-hacker-toolbelt_busybox ; then exit 1; fi
+ echo "Manually add udev tags for snapd-hacker-toolbelt.busybox for kmsg"
+ echo 'KERNEL=="kmsg", TAG+="snap_snapd-hacker-toolbelt_busybox"' > /etc/udev/rules.d/70-spread-test.rules
+ echo "Simulate snapd udev triggers"
+ udevadm control --reload-rules
+ udevadm settle
+ udevadm trigger
+ udevadm settle
+ echo "Verify udev has tag for kmsg"
+ if ! udevadm info /sys/devices/virtual/mem/kmsg | grep snap_snapd-hacker-toolbelt_busybox; then exit 1; fi
+ echo "Run snapd-hacker-toolbelt.busybox echo and see if kmsg added to cgroup"
+ snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello
+ if ! grep 'c 1:11 rwm' /sys/fs/cgroup/devices/snap.snapd-hacker-toolbelt.busybox/devices.list ; then exit 1; fi
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -f /etc/udev/rules.d/70-spread-test.rules
+ udevadm control --reload-rules
+ udevadm settle
+ udevadm trigger
+ udevadm settle
+ # no way to clear cgroup for snapd-hacker-toolbelt atm
--- /dev/null
+summary: The snap named 'core' is preferred to the snap 'ubuntu-core'
+prepare: |
+ snap install --devmode snapd-hacker-toolbelt
+ snap install core
+execute: |
+ snapd-hacker-toolbelt.busybox cat /meta/snap.yaml | grep -q -F 'name: core'
+restore: |
+ snap remove snapd-hacker-toolbelt
+ # XXX: the core snap cannot be removed, we should use a trick to remove it
+ # in some other way but this can wait.
--- /dev/null
+summary: snap-confine honors SNAP_CONFINE_DEBUG environment variable
+execute: |
+ for value in yes no 0 1 unicorn; do
+ SNAP_CONFINE_DEBUG=$value ubuntu-core-launcher blah 2>debug.$value || :
+ done
+ grep -F -q 'DEBUG: shifting arguments by one' debug.yes
+ grep -F -q 'DEBUG: shifting arguments by one' debug.1
+ grep -F -v -q 'DEBUG: shifting arguments by one' debug.no
+ grep -F -v -q 'DEBUG: shifting arguments by one' debug.0
+ grep -F -q 'WARNING: unrecognized value of environment variable SNAP_CONFINE_DEBUG (expected yes/no or 1/0)' debug.unicorn
+restore: |
+ rm -f debug.*
--- /dev/null
+summary: Check that snap-discard-ns gracefully handles errors
+details: |
+ The internal snap-discard-ns program is supposed to simply unmount
+ whatever is mounted at /run/snapd/ns/$SNAP_NAME.mnt. In case of
+ some specific failures though, we don't expect it to fail.
+prepare: |
+ umount /run/snapd/ns || true
+ rm -rf /run/snapd/ns
+execute: |
+ echo "We can try to discard a namespace before *any* snap runs"
+ /usr/lib/snapd/snap-discard-ns foo
+ echo "We can try to discard a namespace before the .mnt file exits"
+ mkdir -p /run/snapd/ns/
+ /usr/lib/snapd/snap-discard-ns foo
+ echo "We can try to discard a namespace before the .mnt file is mounted"
+ touch /run/snapd/ns/foo.mnt
+ /usr/lib/snapd/snap-discard-ns foo
+restore: |
+ rm /run/snapd/ns/foo.mnt
+ rm /run/snapd/ns/foo.lock
+ rmdir /run/snapd/ns
--- /dev/null
+summary: Check that snap-discard-ns works
+# This is blacklisted on debian because debian doesn't use apparmor yet
+systems: [-debian-8]
+details: |
+ The internal snap-discard-ns program is supposed to simply unmount
+ whatever is mounted at /run/snapd/ns/$SNAP_NAME.mnt
+prepare: |
+ mkdir -p /run/snapd/ns/
+ mount --bind /run/snapd/ns /run/snapd/ns
+ mount --make-private /run/snapd/ns
+ touch /run/snapd/ns/foo.mnt
+ unshare --mount=/run/snapd/ns/foo.mnt true
+execute: |
+ /usr/lib/snapd/snap-discard-ns foo
+ ! grep foo.mnt /proc/self/mountinfo
+restore: |
+ umount /run/snapd/ns/foo.mnt || :
+ umount /run/snapd/ns
+ rm /run/snapd/ns/foo.mnt
+ rm /run/snapd/ns/foo.lock
+ # The removal is optional as the directory may contain other files
+ # that we don't want to touch here.
+ rmdir /run/snapd/ns || true
--- /dev/null
+summary: Check that /var/lib/snapd/hostfs is created on demand
+# This is blacklisted on debian because debian doesn't use apparmor yet
+systems: [-debian-8]
+details: |
+ The /var/lib/snapd/hostfs directory is created by snap-confine
+ if the host packaging of snapd doesn't already provide it.
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can move the packaged hostfs directory aside"
+ if [ -d /var/lib/snapd/hostfs ]; then
+ mv /var/lib/snapd/hostfs /var/lib/snapd/hostfs.orig
+ fi
+execute: |
+ cd /
+ echo "We can now run a busybox true just to ensure it started correctly"
+ /snap/bin/snapd-hacker-toolbelt.busybox true
+ echo "We can now check that the directory was created on the system"
+ test -d /var/lib/snapd/hostfs
+restore: |
+ snap remove snapd-hacker-toolbelt
+ if [ -d /var/lib/snapd/hostfs.orig ]; then
+ mv /var/lib/snapd/hostfs.orig /var/lib/snapd/hostfs
+ fi
--- /dev/null
+summary: The /media directory propagates events outwards
+details: |
+ The /media directory is special in that mount events propagate outward from
+ the mount namespace used by snap applications into the main mount
+ namespace.
+prepare: |
+ mkdir -p /media/src
+ mkdir -p /media/dst
+ touch /media/src/canary
+ snap install --devmode snapd-hacker-toolbelt
+execute: |
+ export PATH=$PATH:/snap/bin
+ test ! -e /media/dst/canary
+ snapd-hacker-toolbelt.busybox sh -c 'mount --bind /media/src /media/dst'
+ test -e /media/dst/canary
+restore: |
+ snap remove snapd-hacker-toolbelt
+ # If this doesn't work maybe it is because the test didn't execute correctly
+ umount /media/dst || true
+ rm -f /media/src/canary
+ rmdir /media/src
+ rmdir /media/dst
--- /dev/null
+summary: Check that /media is available to snaps installed in --devmode
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap in devmode"
+ snap install --devmode snapd-hacker-toolbelt
+ echo "Having added a canary file in /media"
+ echo "test" > /media/canary
+execute: |
+ cd /
+ echo "We can see the canary file in /media"
+ [ "$(snapd-hacker-toolbelt.busybox cat /media/canary)" = "test" ]
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -f /media/canary
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/var/lib/snapd/hostfs/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fuse.lxcfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/lxcfs",
+ "mount_src": "lxcfs",
+ "opt_fields": [
+ "master:renumbered/32"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/lxd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/lxd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/lxd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/lxd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/var/lib/snapd/hostfs/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fuse.lxcfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/lxcfs",
+ "mount_src": "lxcfs",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,noatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/lxd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/lxd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/etc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/home"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/lib/modules"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "shared:renumbered/7"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "ro,nosuid,nodev,noexec",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/usr/src",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/usr/src"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib",
+ "mount_src": "none",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/apparmor"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/classic",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/classic"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/cloud"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/console-conf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dbus"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/dhcp"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/extrausers"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initscripts",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/initscripts"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/insserv",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/insserv"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/lxd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/lxd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/machines",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/machines"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/misc"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/pam",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/pam"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/python",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/python"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/resolvconf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/resolvconf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [],
+ "root_dir": "/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/sudo"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/systemd"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ubuntu-fan",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ubuntu-fan"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/ucf",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/ucf"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/update-rc.d",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/update-rc.d"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/urandom",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/urandom"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/vim",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/vim"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK1",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/var/tmp"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/boot/efi",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/boot/grub",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/EFI/ubuntu"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/apparmor.d/cache",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/7"
+ ],
+ "root_dir": "/system-data/etc/apparmor.d/cache"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/system-data/etc/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/dbus-1/system.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/system-data/etc/dbus-1/system.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/default/keyboard",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/system-data/etc/default/keyboard"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/etc/fstab",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/image.fstab"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/hosts",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/system-data/etc/hosts"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/machine-id",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/system-data/etc/machine-id"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/modprobe.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/system-data/etc/modprobe.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/modules-load.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/system-data/etc/modules-load.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/netplan",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/system-data/etc/netplan"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/network/if-up.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/system-data/etc/network/if-up.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/network/interfaces.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/system-data/etc/network/interfaces.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/ppp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/system-data/etc/ppp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/ssh",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/system-data/etc/ssh"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/sudoers.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/system-data/etc/sudoers.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/sysctl.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/system-data/etc/sysctl.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/network",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/system-data/etc/systemd/network"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/system.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/system-data/etc/systemd/system.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/user.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/system-data/etc/systemd/user.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/udev/rules.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/system-data/etc/udev/rules.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/ufw",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/system-data/etc/ufw"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/system-data/etc/writable"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/32"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/33"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/34"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/35"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "shared:renumbered/36"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/mnt",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/37"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/38"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "binfmt_misc",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "binfmt_misc",
+ "opt_fields": [
+ "master:renumbered/40"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/39"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/41"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/42"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/run/cgmanager/fs",
+ "mount_src": "cgmfs",
+ "opt_fields": [
+ "master:renumbered/43"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/44"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/45"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/46"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/50"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/51"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/52"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/53"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/54"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/55"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/56"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/57"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/58"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/59"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/60"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/61"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/62"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/63"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/64"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/65"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/66"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snap.1001_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp/snap.rootfs_XXXXXX",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/snap.rootfs_XXXXXX"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/usr/lib/snapd/snap-confine",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data/zyga/snap-confine"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/cache/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/cache/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/console-conf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dbus"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dhcp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/extrausers"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/misc"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [],
+ "root_dir": "/system-data/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/boot/efi",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/boot/grub",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/EFI/ubuntu"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/var/lib/snapd/hostfs/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/apparmor.d/cache",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/7"
+ ],
+ "root_dir": "/system-data/etc/apparmor.d/cache"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/system-data/etc/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/dbus-1/system.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/system-data/etc/dbus-1/system.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/default/keyboard",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/system-data/etc/default/keyboard"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/fstab",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/image.fstab"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/hosts",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/system-data/etc/hosts"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/machine-id",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/system-data/etc/machine-id"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/modprobe.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/system-data/etc/modprobe.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/modules-load.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/system-data/etc/modules-load.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/netplan",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/system-data/etc/netplan"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/network/if-up.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/system-data/etc/network/if-up.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/network/interfaces.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/system-data/etc/network/interfaces.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/ppp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/system-data/etc/ppp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/ssh",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/system-data/etc/ssh"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/sudoers.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/system-data/etc/sudoers.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/sysctl.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/system-data/etc/sysctl.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/network",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/system-data/etc/systemd/network"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/system-data/etc/systemd/system.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/user.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/system-data/etc/systemd/user.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/udev/rules.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/system-data/etc/udev/rules.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/ufw",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/system-data/etc/ufw"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/system-data/etc/writable"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/home",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/32"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/33"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/34"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/35"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/mnt",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/37"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/38"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "binfmt_misc",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc",
+ "mount_src": "binfmt_misc",
+ "opt_fields": [
+ "master:renumbered/40"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/39"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/root",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/41"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/42"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/cgmanager/fs",
+ "mount_src": "cgmfs",
+ "opt_fields": [
+ "master:renumbered/43"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/44"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/45"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/46"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop5",
+ "opt_fields": [
+ "master:renumbered/68"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop6",
+ "opt_fields": [
+ "master:renumbered/69"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/50"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/51"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/52"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/53"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/54"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/55"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/56"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/57"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/58"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/59"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/60"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/61"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/62"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/63"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/64"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/65"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/66"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/tmp",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/tmp/snap.rootfs_XXXXXX",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/snap.rootfs_XXXXXX"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/usr/lib/snapd/snap-confine",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data/zyga/snap-confine"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/cache/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/cache/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/console-conf",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/console-conf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/dbus",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dbus"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/dhcp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dhcp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/extrausers",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/extrausers"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/initramfs-tools",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/logrotate",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/misc",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/misc"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/snapd",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/snapd"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/sudo",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/70"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/random-seed",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/random-seed"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/rfkill",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/rfkill"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/waagent",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/log",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/tmp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop5",
+ "opt_fields": [
+ "master:renumbered/68"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop6",
+ "opt_fields": [
+ "master:renumbered/69"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/70"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd/random-seed",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/random-seed"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd/rfkill",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/rfkill"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop5",
+ "opt_fields": [
+ "master:renumbered/68"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop6",
+ "opt_fields": [
+ "master:renumbered/69"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ }
+]
--- /dev/null
+[
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/boot/efi",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/boot/grub",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/EFI/ubuntu"
+ },
+ {
+ "fs_type": "devtmpfs",
+ "mount_opts": "rw,nosuid,relatime",
+ "mount_point": "/dev",
+ "mount_src": "udev",
+ "opt_fields": [
+ "master:renumbered/2"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "hugetlbfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/hugepages",
+ "mount_src": "hugetlbfs",
+ "opt_fields": [
+ "master:renumbered/3"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "mqueue",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/mqueue",
+ "mount_src": "mqueue",
+ "opt_fields": [
+ "master:renumbered/4"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/ptmx",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/ptmx"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [
+ "master:renumbered/5"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "devpts",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/dev/pts",
+ "mount_src": "devpts",
+ "opt_fields": [],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev",
+ "mount_point": "/dev/shm",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/6"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/etc/alternatives",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/etc/alternatives"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/apparmor.d/cache",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/7"
+ ],
+ "root_dir": "/system-data/etc/apparmor.d/cache"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/system-data/etc/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/dbus-1/system.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/system-data/etc/dbus-1/system.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/default/keyboard",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/system-data/etc/default/keyboard"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/etc/fstab",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/image.fstab"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/hosts",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/system-data/etc/hosts"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/machine-id",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/system-data/etc/machine-id"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/modprobe.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/system-data/etc/modprobe.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/modules-load.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/system-data/etc/modules-load.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/netplan",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/system-data/etc/netplan"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/network/if-up.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/system-data/etc/network/if-up.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/network/interfaces.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/system-data/etc/network/interfaces.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/ppp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/system-data/etc/ppp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/ssh",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/system-data/etc/ssh"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/sudoers.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/system-data/etc/sudoers.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/sysctl.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/system-data/etc/sysctl.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/network",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/system-data/etc/systemd/network"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/system.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/system-data/etc/systemd/system.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/systemd/user.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/system-data/etc/systemd/user.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/udev/rules.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/system-data/etc/udev/rules.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/ufw",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/system-data/etc/ufw"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/etc/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/system-data/etc/writable"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/home",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/32"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/33"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/34"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/35"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/media",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "shared:renumbered/36"
+ ],
+ "root_dir": "/media"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/mnt",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/37"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "proc",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/proc",
+ "mount_src": "proc",
+ "opt_fields": [
+ "master:renumbered/38"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "binfmt_misc",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "binfmt_misc",
+ "opt_fields": [
+ "master:renumbered/40"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "autofs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/proc/sys/fs/binfmt_misc",
+ "mount_src": "systemd-1",
+ "opt_fields": [
+ "master:renumbered/39"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/root",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/41"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/42"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/run/cgmanager/fs",
+ "mount_src": "cgmfs",
+ "opt_fields": [
+ "master:renumbered/43"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/44"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/45"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/46"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "sysfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys",
+ "mount_src": "sysfs",
+ "opt_fields": [
+ "master:renumbered/50"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw",
+ "mount_point": "/sys/fs/cgroup",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/51"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/blkio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/52"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpu,cpuacct",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/53"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/cpuset",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/54"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/devices",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/55"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/freezer",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/56"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/hugetlb",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/57"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/memory",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/58"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/net_cls,net_prio",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/59"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/perf_event",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/60"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/pids",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/61"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "cgroup",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/cgroup/systemd",
+ "mount_src": "cgroup",
+ "opt_fields": [
+ "master:renumbered/62"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "fusectl",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/fs/fuse/connections",
+ "mount_src": "fusectl",
+ "opt_fields": [
+ "master:renumbered/63"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "pstore",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/fs/pstore",
+ "mount_src": "pstore",
+ "opt_fields": [
+ "master:renumbered/64"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "debugfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/sys/kernel/debug",
+ "mount_src": "debugfs",
+ "opt_fields": [
+ "master:renumbered/65"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "securityfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/sys/kernel/security",
+ "mount_src": "securityfs",
+ "opt_fields": [
+ "master:renumbered/66"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snap.1001_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/tmp/snap.rootfs_XXXXXX",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/snap.rootfs_XXXXXX"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/usr/lib/snapd/snap-confine",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data/zyga/snap-confine"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/cache/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/cache/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/console-conf",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/console-conf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dbus",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dbus"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/dhcp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dhcp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/extrausers",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/extrausers"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/initramfs-tools",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/logrotate",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/misc",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/misc"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/snapd"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [],
+ "root_dir": "/system-data/var/lib/snapd/hostfs"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "ro,relatime",
+ "mount_point": "/var/lib/snapd/hostfs",
+ "mount_src": "/dev/remapped-loop0",
+ "opt_fields": [
+ "master:renumbered/0"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/boot/efi",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "vfat",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/boot/grub",
+ "mount_src": "/dev/BLOCK2",
+ "opt_fields": [
+ "master:renumbered/1"
+ ],
+ "root_dir": "/EFI/ubuntu"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/apparmor.d/cache",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/7"
+ ],
+ "root_dir": "/system-data/etc/apparmor.d/cache"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/8"
+ ],
+ "root_dir": "/system-data/etc/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/dbus-1/system.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/9"
+ ],
+ "root_dir": "/system-data/etc/dbus-1/system.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/default/keyboard",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/10"
+ ],
+ "root_dir": "/system-data/etc/default/keyboard"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/fstab",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/11"
+ ],
+ "root_dir": "/image.fstab"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/hosts",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/12"
+ ],
+ "root_dir": "/system-data/etc/hosts"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/machine-id",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/13"
+ ],
+ "root_dir": "/system-data/etc/machine-id"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/modprobe.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/14"
+ ],
+ "root_dir": "/system-data/etc/modprobe.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/modules-load.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/15"
+ ],
+ "root_dir": "/system-data/etc/modules-load.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/netplan",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/16"
+ ],
+ "root_dir": "/system-data/etc/netplan"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/network/if-up.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/17"
+ ],
+ "root_dir": "/system-data/etc/network/if-up.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/network/interfaces.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/18"
+ ],
+ "root_dir": "/system-data/etc/network/interfaces.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/ppp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/19"
+ ],
+ "root_dir": "/system-data/etc/ppp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/ssh",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/20"
+ ],
+ "root_dir": "/system-data/etc/ssh"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/sudoers.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/21"
+ ],
+ "root_dir": "/system-data/etc/sudoers.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/sysctl.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/22"
+ ],
+ "root_dir": "/system-data/etc/sysctl.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/network",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/23"
+ ],
+ "root_dir": "/system-data/etc/systemd/network"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/24"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/25"
+ ],
+ "root_dir": "/system-data/etc/systemd/system"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/26"
+ ],
+ "root_dir": "/system-data/etc/systemd/system.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/systemd/user.conf.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/27"
+ ],
+ "root_dir": "/system-data/etc/systemd/user.conf.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/udev/rules.d",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/28"
+ ],
+ "root_dir": "/system-data/etc/udev/rules.d"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/ufw",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/29"
+ ],
+ "root_dir": "/system-data/etc/ufw"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/etc/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/30"
+ ],
+ "root_dir": "/system-data/etc/writable"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/home",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/32"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/firmware",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/33"
+ ],
+ "root_dir": "/firmware"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/34"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/lib/modules",
+ "mount_src": "/dev/remapped-loop1",
+ "opt_fields": [
+ "master:renumbered/35"
+ ],
+ "root_dir": "/modules"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/mnt",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/37"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/root",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/root"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/41"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/42"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/cgmanager/fs",
+ "mount_src": "cgmfs",
+ "opt_fields": [
+ "master:renumbered/43"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/lock",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/44"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,noexec,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns",
+ "mount_src": "tmpfs",
+ "opt_fields": [],
+ "root_dir": "/snapd/ns"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/45"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,nosuid,nodev,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/46"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/snap"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop5",
+ "opt_fields": [
+ "master:renumbered/68"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop6",
+ "opt_fields": [
+ "master:renumbered/69"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/tmp",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/tmp/snap.rootfs_XXXXXX",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/67"
+ ],
+ "root_dir": "/snap.rootfs_XXXXXX"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/usr/lib/snapd/snap-confine",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/user-data/zyga/snap-confine"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/cache/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/cache/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/apparmor",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/apparmor"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/cloud",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/cloud"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/console-conf",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/console-conf"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/dbus",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dbus"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/dhcp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/dhcp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/extrausers",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/extrausers"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/initramfs-tools",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/initramfs-tools"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/logrotate",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/logrotate"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/misc",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/misc"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/snapd",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/snapd"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/sudo",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/70"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/random-seed",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/random-seed"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/rfkill",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/rfkill"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/lib/waagent",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/log",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/var/tmp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop5",
+ "opt_fields": [
+ "master:renumbered/68"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop6",
+ "opt_fields": [
+ "master:renumbered/69"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "tmpfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/sudo",
+ "mount_src": "tmpfs",
+ "opt_fields": [
+ "master:renumbered/70"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd/random-seed",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/random-seed"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/systemd/rfkill",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/systemd/rfkill"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/lib/waagent",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/lib/waagent"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/log",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/log"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/snap",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/snap"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/var/tmp",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/system-data/var/tmp"
+ },
+ {
+ "fs_type": "ext4",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable",
+ "mount_src": "/dev/BLOCK3",
+ "opt_fields": [
+ "master:renumbered/31"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop5",
+ "opt_fields": [
+ "master:renumbered/68"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/core/NUMBER",
+ "mount_src": "/dev/remapped-loop6",
+ "opt_fields": [
+ "master:renumbered/69"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/pc-kernel/NUMBER",
+ "mount_src": "/dev/remapped-loop2",
+ "opt_fields": [
+ "master:renumbered/47"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/pc/NUMBER",
+ "mount_src": "/dev/remapped-loop3",
+ "opt_fields": [
+ "master:renumbered/48"
+ ],
+ "root_dir": "/"
+ },
+ {
+ "fs_type": "squashfs",
+ "mount_opts": "rw,relatime",
+ "mount_point": "/writable/system-data/snap/snapd-hacker-toolbelt/x1",
+ "mount_src": "/dev/remapped-loop4",
+ "opt_fields": [
+ "master:renumbered/49"
+ ],
+ "root_dir": "/"
+ }
+]
--- /dev/null
+#!/usr/bin/env python3
+import sys
+import json
+import re
+
+class mountinfo_entry:
+
+ def __init__(self, fs_type, mount_id, mount_opts, mount_point, mount_src, opt_fields, root_dir):
+ self.fs_type = fs_type
+ self.mount_id = mount_id
+ self.mount_opts = mount_opts
+ self.mount_point = mount_point
+ self.mount_src = mount_src
+ self.opt_fields = opt_fields
+ self.root_dir = root_dir
+
+ @classmethod
+ def parse(cls, line):
+ parts = line.split()
+ fs_type = parts[-3]
+ mount_id = parts[0]
+ mount_opts = parts[5]
+ mount_point = parts[4]
+ mount_src = parts[-2]
+ root_dir = parts[3]
+ opt_fields = []
+ i = 6
+ while parts[i] != '-':
+ opt = parts[i]
+ opt_fields.append(opt)
+ i += 1
+ opt_fields.sort()
+ return cls(fs_type, mount_id, mount_opts, mount_point, mount_src,
+ opt_fields, root_dir)
+
+ def _fix_nondeterministic_mount_point(self):
+ self.mount_point = re.sub('_\w{6}', '_XXXXXX', self.mount_point)
+ self.mount_point = re.sub('/\d+$', '/NUMBER', self.mount_point)
+
+ def _fix_nondeterministic_root_dir(self):
+ self.root_dir = re.sub('_\w{6}', '_XXXXXX', self.root_dir)
+
+ def _fix_nondeterministic_mount_src(self):
+ self.mount_src = re.sub('/dev/[sv]da', '/dev/BLOCK', self.mount_src)
+
+ def _fix_nondeterministic_opt_fields(self, seen):
+ fixed = []
+ for opt in self.opt_fields:
+ if opt not in seen:
+ opt_id = len(seen)
+ seen[opt] = opt_id
+ else:
+ opt_id = seen[opt]
+ remapped_opt = re.sub(':\d+$', lambda m: ':renumbered/{}'.format(opt_id), opt)
+ fixed.append(remapped_opt)
+ self.opt_fields = fixed
+
+ def _fix_nondeterministic_loop(self, seen):
+ if not self.mount_src.startswith("/dev/loop"):
+ return
+ if self.mount_src not in seen:
+ loop_id = len(seen)
+ seen[self.mount_src] = loop_id
+ else:
+ loop_id = seen[self.mount_src]
+ self.mount_src = re.sub('loop\d+$', lambda m: 'remapped-loop{}'.format(loop_id), self.mount_src)
+
+ def as_json(self):
+ return {
+ "fs_type": self.fs_type,
+ "mount_opts": self.mount_opts,
+ "mount_point": self.mount_point,
+ "mount_src": self.mount_src,
+ "opt_fields": self.opt_fields,
+ "root_dir": self.root_dir,
+ }
+
+
+def parse_mountinfo(lines):
+ return [mountinfo_entry.parse(line) for line in lines]
+
+
+def fix_initial_nondeterminism(entries):
+ for entry in entries:
+ entry._fix_nondeterministic_mount_point()
+
+
+def fix_remaining_nondeterminism(entries):
+ seen_opt_fields = {}
+ seen_loops = {}
+ for entry in entries:
+ entry._fix_nondeterministic_root_dir()
+ entry._fix_nondeterministic_mount_src()
+ entry._fix_nondeterministic_opt_fields(seen_opt_fields)
+ entry._fix_nondeterministic_loop(seen_loops)
+
+
+def main():
+ entries = parse_mountinfo(sys.stdin)
+ # Get rid of the core snap as it is not certain that we'll see one and we want determinism
+ entries = [entry for entry in entries if not re.match("/snap/core/\d+", entry.mount_point)]
+ # Fix random directories and non-deterministic revisions
+ fix_initial_nondeterminism(entries)
+ # Sort by just the mount point,
+ entries.sort(key=lambda entry: (entry.mount_point))
+ # Fix remainder of the non-determinism
+ fix_remaining_nondeterminism(entries)
+ # Make entries nicely deterministic, by sorting them by mount location
+ entries.sort(key=lambda entry: (entry.mount_point, entry.mount_src, entry.root_dir))
+ # Export everything
+ json.dump([entry.as_json() for entry in entries],
+ sys.stdout, sort_keys=True, indent=2, separators=(',', ': '))
+ sys.stdout.write('\n')
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null
+#!/usr/bin/env python3
+import os
+import sys
+
+def main():
+ kernel_arch = os.uname().machine
+ # Because off by one bugs and naming ...
+ snap_arch_map = {
+ 'aarch64': 'arm64',
+ 'armv7l': 'armhf',
+ 'x86_64': 'amd64',
+ 'i686': 'i386',
+ }
+ try:
+ print(snap_arch_map[kernel_arch])
+ except KeyError:
+ print("unsupported kernel architecture: {!a}".format(kernel_arch), file=sys.stderr)
+ return 1
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null
+summary: Ensure that the mount namespace a given layout
+details: |
+ This test analyzes /proc/self/mountinfo which contains a representation of
+ the mount table of the current process. The mount table is a very sensitive
+ part of the confinement design. This test measures the effective table,
+ normalizes it (to remove some inherent randomness of certain identifiers
+ and make it uniform regardless of particular names of block devices, snap
+ revisions, etc.) and then compares it to a canned copy.
+
+ There are several reference tables, one for core (aka all-snap system) and
+ one for classic. At this time only classic systems are measured and tested.
+ The classic systems are further divided into those using the core snap and
+ those using the older ubuntu-core snap. Lastly, they are divided by
+ architectures to take account any architecture specific differences.
+prepare: |
+ echo "Having installed a busybox"
+ snap install snapd-hacker-toolbelt
+execute: |
+ echo "We can map the kernel architecture name to snap architecture name"
+ arch=$(./snap-arch.py)
+ echo "We can run busybox true so that snap-confine creates a mount namespace"
+ snapd-hacker-toolbelt.busybox true
+ echo "Using nsenter we can move to that namespace, inspect and normalize the mount table"
+ nsenter -m/run/snapd/ns/snapd-hacker-toolbelt.mnt \
+ cat /proc/self/mountinfo | ./process.py > observed.json
+ echo "We can now compare the obtained mount table to expected values"
+ if [ -e /snap/core/current ]; then
+ cmp observed.json expected.classic.core.$SPREAD_BACKEND.$arch.json
+ else
+ cmp observed.json expected.classic.ubuntu-core.$SPREAD_BACKEND.$arch.json
+ fi
+debug: |
+ echo "When something goes wrong we can display a human-readable diff"
+ arch=$(./snap-arch.py)
+ if [ -e /snap/core/current ]; then
+ diff -u observed.json expected.classic.core.$SPREAD_BACKEND.$arch.json || :
+ else
+ diff -u observed.json expected.classic.ubuntu-core.$SPREAD_BACKEND.$arch.json || :
+ fi
+ echo "And pastebin the raw table for analysis"
+ apt-get install pastebinit
+ nsenter -m/run/snapd/ns/snapd-hacker-toolbelt.mnt \
+ cat /proc/self/mountinfo | pastebinit
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -f observed.json
--- /dev/null
+summary: mount namespace is shared among processes
+details: |
+ The mount namespace is automatically shared amongst processes belonging to
+ a given snap. The namespace is preserved until the machine reboots or until
+ it is discarded with snap-discard-ns.
+prepare: |
+ # NOTE: devmode is required because otherwise we cannot read /proc/self/ns/mnt
+ snap install --devmode snapd-hacker-toolbelt
+execute: |
+ export PATH=/snap/bin:$PATH
+ echo "The mount namespace inside a snap is different"
+ outer_mnt_ns=$(readlink /proc/self/ns/mnt)
+ inner_mnt_ns=$(snapd-hacker-toolbelt.busybox readlink /proc/self/ns/mnt)
+ [ "$outer_mnt_ns" != "$inner_mnt_ns" ]
+ echo "The mount namespace is stable across invocations"
+ for i in $(seq 100); do
+ [ "$inner_mnt_ns" = "$(snapd-hacker-toolbelt.busybox readlink /proc/self/ns/mnt)" ]
+ done
+restore: |
+ snap remove snapd-hacker-toolbelt
--- /dev/null
+summary: Apparmor profile prevents bind-mounting to /snap/bin
+# This is blacklisted on debian because it relies on apparmor mount mediation
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can change its mount profile externally to create bind mount /snap/bin somewhere"
+ echo "/snap/snapd-hacker-toolbelt/mnt -> /snap/bin"
+ mkdir -p /var/lib/snapd/mount
+ echo "/snap/snapd-hacker-toolbelt/current/mnt /snap/bin none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+execute: |
+ cd /
+ echo "Let's clear the kernel ring buffer"
+ dmesg -c
+ echo "We can now run busybox true and expect it to fail"
+ orig_ratelimit=$(sysctl -n kernel.printk_ratelimit)
+ sysctl -w kernel.printk_ratelimit=0
+ ! /snap/bin/snapd-hacker-toolbelt.busybox true
+ sysctl -w kernel.printk_ratelimit=$orig_ratelimit
+ echo "Not only the command failed because snap-confine failed, we see why!"
+ dmesg | grep 'apparmor="DENIED" operation="mount" info="failed srcname match" error=-13 profile="/usr/lib/snapd/snap-confine" name="/snap/bin/" pid=[0-9]\+ comm="ubuntu-core-lau" srcname="/snap/snapd-hacker-toolbelt/[0-9]\+/mnt/" flags="rw, bind"'
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+ dmesg -c
--- /dev/null
+summary: Apparmor profile prevents bind-mounting from /snap/bin
+# This is blacklisted on debian because it relies on apparmor mount mediation
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can change its mount profile externally to create bind mount /snap/bin somewhere"
+ echo "/snap/bin -> /snap/snapd-hacker-toolbelt/mnt"
+ mkdir -p /var/lib/snapd/mount
+ echo "/snap/bin /snap/snapd-hacker-toolbelt/current/mnt none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+execute: |
+ cd /
+ echo "Let's clear the kernel ring buffer"
+ dmesg -c
+ echo "We can now run busybox true and expect it to fail"
+ orig_ratelimit=$(sysctl -n kernel.printk_ratelimit)
+ sysctl -w kernel.printk_ratelimit=0
+ ! /snap/bin/snapd-hacker-toolbelt.busybox true
+ sysctl -w kernel.printk_ratelimit=$orig_ratelimit
+ echo "Not only the command failed because snap-confine failed, we see why!"
+ dmesg | grep 'apparmor="DENIED" operation="mount" info="failed srcname match" error=-13 profile="/usr/lib/snapd/snap-confine" name="/snap/snapd-hacker-toolbelt/[0-9]\+/mnt/" pid=[0-9]\+ comm="ubuntu-core-lau" srcname="/snap/bin/" flags="rw, bind"'
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+ dmesg -c
--- /dev/null
+summary: Check that missing destination directory aborts mount processing
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can change its mount profile externally to create a read-only bind-mount"
+ echo "/var/snap/snapd-hacker-toolbelt/common/src -> /var/snap/snapd-hacker-toolbelt/common/dst"
+ mkdir -p /var/lib/snapd/mount
+ echo "/var/snap/snapd-hacker-toolbelt/common/src /var/snap/snapd-hacker-toolbelt/common/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+ echo "We can now create the source directory, missing the destination directory"
+ mkdir -p /var/snap/snapd-hacker-toolbelt/common/src
+execute: |
+ echo "We can now run busybox.true and expect it to fail"
+ ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true )
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
--- /dev/null
+summary: Check that missing source directory aborts mount processing
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can change its mount profile externally to create a read-only bind-mount"
+ echo "/var/snap/snapd-hacker-toolbelt/common/src -> /var/snap/snapd-hacker-toolbelt/common/dst"
+ mkdir -p /var/lib/snapd/mount
+ echo "/var/snap/snapd-hacker-toolbelt/common/src /var/snap/snapd-hacker-toolbelt/common/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+ echo "We can now create the destination directory, missing the source directory"
+ mkdir -p /var/snap/snapd-hacker-toolbelt/common/dst
+execute: |
+ echo "We can now run busybox.true and expect it to fail"
+ ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true )
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
--- /dev/null
+summary: Check that mount profiles cannot be used to mount tmpfs
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+execute: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap list | grep -q snapd-hacker-toolbelt || snap install snapd-hacker-toolbelt
+
+ echo "We can change its mount profile externally to mount tmpfs at /var/snap/snapd-hacker-toolbelt/mnt"
+ mkdir -p /var/lib/snapd/mount
+ echo "none /var/snap/snapd-hacker-toolbelt/common/mnt tmpfs rw 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+
+ echo "We can now create the test mount directory"
+ mkdir -p /var/snap/snapd-hacker-toolbelt/common/mnt
+
+ echo "We can now run busybox.true and expect it to fail"
+ ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true )
--- /dev/null
+summary: Check that read-only bind mounts can be created
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can change its mount profile externally to create a read-only bind-mount"
+ echo "/snap/snapd-hacker-toolbelt/current/src -> /snap/snapd-hacker-toolbelt/current/dst"
+ mkdir -p /var/lib/snapd/mount
+ echo "/snap/snapd-hacker-toolbelt/current/src /snap/snapd-hacker-toolbelt/current/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+execute: |
+ cd /
+ echo "We can now look at the .id file in the destination directory"
+ [ "$(/snap/bin/snapd-hacker-toolbelt.busybox cat /snap/snapd-hacker-toolbelt/current/dst/.id)" = "source" ]
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
--- /dev/null
+summary: Check that read-write bind mounts can be created
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "We can connect it to the mount-observe slot from the core"
+ snap connect snapd-hacker-toolbelt:mount-observe ubuntu-core:mount-observe
+ echo "We can change its mount profile externally to create a read-only bind-mount"
+ echo "/snap/snapd-hacker-toolbelt/current/src -> /snap/snapd-hacker-toolbelt/current/dst"
+ mkdir -p /var/lib/snapd/mount
+ echo "/snap/snapd-hacker-toolbelt/current/src /snap/snapd-hacker-toolbelt/current/dst none bind,rw 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
+execute: |
+ cd /
+ echo "We can now look at the .id file in the destination directory"
+ [ "$(/snap/bin/snapd-hacker-toolbelt.busybox cat /snap/snapd-hacker-toolbelt/current/dst/.id)" = "source" ]
+ echo "As well as the current mount points"
+ # FIXME: this doesn't show 'rw', bind mounts confuse most tools and it
+ # seems that busybox is not any different here.
+ /snap/bin/snapd-hacker-toolbelt.busybox mount | grep snapd-hacker-toolbelt
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf /var/snap/snapd-hacker-toolbelt
+ rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab
--- /dev/null
+summary: Check for https://bugs.launchpad.net/snap-confine/+bug/1597842
+# This is blacklisted on debian because debian doesn't use apparmor yet
+systems: [-debian-8]
+details: |
+ The snappy execution environment should contain the /usr/src directory
+ from the host filesystem when running on a classic distribution.
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "and having connected the mount-observe interface"
+ snap connect snapd-hacker-toolbelt:mount-observe ubuntu-core:mount-observe
+execute: |
+ cd /
+ echo "We can ensure that /usr/src is mounted"
+ /snap/bin/snapd-hacker-toolbelt.busybox cat /proc/self/mounts | grep ' /usr/src '
+restore: |
+ snap remove snapd-hacker-toolbelt
--- /dev/null
+summary: Check that basic install works
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+#
+# This test only makes sense on x86_64 as it can execute i386 code in addition
+# to native x86_64 code).
+systems: [-debian-8, -ubuntu-16.04-32]
+prepare: |
+ snap install --edge test-seccomp-compat
+execute: |
+ cd /
+ echo Run the 64 bit binary
+ test-seccomp-compat.true64
+ echo Run the 32 bit binary
+ test-seccomp-compat.true32
+restore: |
+ snap remove test-seccomp-compat
--- /dev/null
+summary: Check that basic install works
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+prepare: |
+ snap install snapd-hacker-toolbelt
+execute: |
+ cd /
+ echo Run some hello-world stuff
+ snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello
+ snapd-hacker-toolbelt.busybox env | grep SNAP_NAME=snapd-hacker-toolbelt
+ echo Ensure that we get an error if we try to abuse the sandbox
+ if snapd-hacker-toolbelt.busybox touch /var/tmp/evil; then exit 1; fi
+ dmesg -c
+restore: |
+ snap remove snapd-hacker-toolbelt
--- /dev/null
+summary: Check that ubuntu-core-launcher executes correctly
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+execute: |
+ echo "ubuntu-core-launcher is installed and responds to --help"
+ ubuntu-core-launcher --help 2>&1 | grep -F -q 'Usage: ubuntu-core-launcher <security-tag> <binary>'
--- /dev/null
+summary: Ensure that SNAP_USER_DATA directory is created by snap-confine
+# This is blacklisted on debian because debian doesn't use apparmor yet
+systems: [-debian-8]
+details: |
+ A regression was found in snap-confine where the new code path in snapd was
+ not active yet but the corresponding code path in snap-confine was already
+ removed. This resulted in the $SNAP_USER_DATA directory not being created
+ at runtime.
+ This test checks that it is actually created
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "Having removed the SNAP_USER_DATA directory"
+ rm -rf "$HOME/snap/snapd-hacker-toolbelt/"
+execute: |
+ cd /
+ echo "We can now run snapd-hacker-toolbelt.busybox true"
+ /snap/bin/snapd-hacker-toolbelt.busybox true
+ echo "And see that the SNAP_USER_DATA directory was created"
+ test -d $HOME/snap/snapd-hacker-toolbelt
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf "$HOME/snap/snapd-hacker-toolbelt/"
--- /dev/null
+summary: Ensure that XDG_RUNTIME_DIR directory is created by snap-confine
+# This is blacklisted on debian because debian doesn't use apparmor yet
+systems: [-debian-8]
+details: |
+ This test checks that XDG_RUNTIME_DIR is actually created
+prepare: |
+ echo "Having installed the snapd-hacker-toolbelt snap"
+ snap install snapd-hacker-toolbelt
+ echo "Having removed the XDG_RUNTIME_DIR directory"
+ rm -rf "/run/user/`id -u`/snapd-hacker-toolbelt"
+execute: |
+ cd /
+ echo "FIXME: export XDG_RUNTIME_DIR for now until snapd does it"
+ export XDG_RUNTIME_DIR="/run/user/`id -u`/snapd-hacker-toolbelt"
+ echo "We can now run snapd-hacker-toolbelt.busybox true"
+ /snap/bin/snapd-hacker-toolbelt.busybox true
+ echo "And see that the XDG_RUNTIME_DIR directory was created"
+ test -d /run/user/`id -u`/snapd-hacker-toolbelt
+restore: |
+ snap remove snapd-hacker-toolbelt
+ rm -rf "/run/user/`id -u`/snapd-hacker-toolbelt"
--- /dev/null
+summary: snap-confine supports the --version switch
+prepare: |
+ [ "$(ubuntu-core-launcher --version)" = "snap-confine $(cat $SPREAD_PATH/VERSION)" ]
--- /dev/null
+summary: Check that execle doesn't regress
+# This is blacklisted on debian because we first have to get the dpkg-vendor patches
+systems: [-debian-8]
+details: |
+ The setup for this test is unorthodox because by the time the cgroup code is
+ executed, the mounts are in place and /lib/udev/snappy-app-dev from the core
+ snap is used. Unfortunately, simple bind mounts over
+ /snap/ubuntu-core/current/lib/udev don't work and the core snap must be
+ unpacked, lib/udev/snappy-app-dev modified to be tested, repacked and mounted.
+ We unmount the core snap and move it aside to avoid both the original and the
+ updated core snap from being mounted on the same mount point, which confuses
+ the kernel.
+prepare: |
+ echo "This test is disabled because it causes failures for subsequent tests"
+ echo "it seems to unmount ubuntu-core snap and not re-mount the original one correctly"
+ exit 0
+ cd /
+ echo "Install hello-world"
+ snap install hello-world
+ systemctl stop snapd.refresh.timer snapd.service snapd.socket
+ # all of this ls madness can go away when we have remote environment
+ # variables
+ echo "Unmount original core snap"
+ umount $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1)
+ mv $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1).orig
+ echo "Create modified core snap for snappy-app-dev"
+ unsquashfs $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1)
+ echo 'echo PATH=$PATH > /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snappy-app-dev
+ echo 'echo TESTVAR=$TESTVAR >> /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snappy-app-dev
+ mksquashfs ./squashfs-root $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') -comp xz
+ if [ ! -e $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) ]; then exit 1; fi
+ echo "Mount modified core snap"
+ mount $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1)
+ systemctl start snapd.refresh.timer snapd.service snapd.socket
+execute: |
+ exit 0
+ cd /
+ echo "Add a udev tag so affected code branch is exercised"
+ echo 'KERNEL=="uinput", TAG+="snap_hello-world_env"' > /etc/udev/rules.d/70-spread-test.rules
+ udevadm control --reload-rules
+ udevadm settle
+ udevadm trigger
+ udevadm settle
+ PATH=/foo:$PATH TESTVAR=bar hello-world.env | grep PATH
+ cat /run/udev/spread-test.out
+ echo "Ensure user-specified PATH is not used"
+ ! grep 'PATH=/foo' /run/udev/spread-test.out
+ echo "Ensure environment is clean"
+ ! grep 'TESTVAR=bar' /run/udev/spread-test.out
+restore: |
+ exit 0
+ echo "Remove hello-world"
+ snap remove hello-world
+ systemctl stop snapd.refresh.timer snapd.service snapd.socket
+ echo "Unmount the modified core snap"
+ # all of this ls madness can go away when we have remote environment
+ # variables
+ umount $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1)
+ if [ "x"$(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) != "x" ]; then mv -f $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') ; fi
+ echo "Mount the original core snap"
+ mount $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1)
+ rm -rf /squashfs-root
+ rm -f /run/udev/spread-test.out
+ rm -f /etc/udev/rules.d/70-spread-test.rules
+ udevadm control --reload-rules
+ udevadm settle
+ udevadm trigger
+ udevadm settle
+ systemctl start snapd.refresh.timer snapd.service snapd.socket
--- /dev/null
+#!/bin/sh
+# This script creates a new release tarball
+set -xue
+
+# Sanity check, are we in the top-level directory of the tree?
+test -f configure.ac || ( echo 'this script must be executed from the top-level of the tree' && exit 1)
+
+# Record where the top level directory is
+top_dir=$(pwd)
+
+# Create source distribution tarball and place it in the top-level directory.
+create_dist_tarball() {
+ # Load the version number from a dedicated file
+ local pkg_version=
+ pkg_version="$(cat "$top_dir/VERSION")"
+
+ # Ensure that build system is up-to-date and ready
+ autoreconf -f -i
+ # XXX: This fixes somewhat odd error when configure below (in an empty directory) fails with:
+ # configure: error: source directory already configured; run "make distclean" there first
+ test -f Makefile && make distclean
+
+ # Create a scratch space to run configure
+ scratch_dir="$(mktemp -d)"
+ trap 'rm -rf "$scratch_dir"' EXIT
+
+ # Configure the project in a scratch directory
+ cd "$scratch_dir"
+ "$top_dir/configure" --prefix=/usr
+
+ # Create the distribution tarball
+ make dist
+
+ # Ensure we got the tarball we were expecting to see
+ test -f "snap-confine-$pkg_version.tar.gz"
+
+ # Move it to the top-level directory
+ mv "snap-confine-$pkg_version.tar.gz" "$top_dir/"
+}
+
+create_dist_tarball
--- /dev/null
+#!/bin/sh
+# This script is started by spread to prepare the execution environment
+set -xue
+
+# Sanity check, are we in the top-level directory of the tree?
+test -f configure.ac || ( echo 'this script must be executed from the top-level of the tree' && exit 1)
+
+# Record where the top level directory is
+top_dir=$(pwd)
+
+# Record the current distribution release data to know what to do
+release_ID="$( . /etc/os-release && echo "${ID:-linux}" )"
+release_VERSION_ID="$( . /etc/os-release && echo "${VERSION_ID:-}" )"
+
+
+build_debian_or_ubuntu_package() {
+ local pkg_version
+ local distro_packaging_git_branch
+ local distro_packaging_git
+ local distro_archive
+ local distro_codename
+ local sbuild_createchroot_extra=""
+ pkg_version="$(cat "$top_dir/VERSION")"
+
+ if [ ! -f "$top_dir/spread-tests/distros/$release_ID.$release_VERSION_ID" ] || \
+ [ ! -f "$top_dir/spread-tests/distros/$release_ID.common" ]; then
+ echo "Distribution: $release_ID (release $release_VERSION_ID) is not supported"
+ echo "please read this script and create new files in spread-test/distros"
+ exit 1
+ fi
+
+ # source the distro specific vars
+ . "$top_dir/spread-tests/distros/$release_ID.$release_VERSION_ID"
+ . "$top_dir/spread-tests/distros/$release_ID.common"
+
+ # sanity check, ensure that essential variables were defined
+ test -n "$distro_packaging_git_branch"
+ test -n "$distro_packaging_git"
+ test -n "$distro_archive"
+ test -n "$distro_codename"
+
+ # Create a scratch space
+ scratch_dir="$(mktemp -d)"
+ trap 'rm -rf "$scratch_dir"' EXIT
+
+ # Do everything in the scratch directory
+ cd "$scratch_dir"
+
+ # Fetch the current Ubuntu packaging for the appropriate release
+ git clone -b "$distro_packaging_git_branch" "$distro_packaging_git" distro-packaging
+
+ # Install all the build dependencies declared by the package.
+ apt-get install --quiet -y gdebi-core
+ apt-get install --quiet -y $(gdebi --quiet --apt-line ./distro-packaging/debian/control)
+
+ # Generate a new upstream tarball from the current state of the tree
+ ( cd "$top_dir" && spread-tests/release.sh )
+
+ # Prepare the .orig tarball and unpackaged source tree
+ cp "$top_dir/snap-confine-$pkg_version.tar.gz" "snap-confine_$pkg_version.orig.tar.gz"
+ tar -zxf "snap-confine_$pkg_version.orig.tar.gz"
+
+ # Apply the debian directory from downstream packaging to form a complete source package
+ mv "distro-packaging/debian" "snap-confine-$pkg_version/debian"
+ rm -rf distro-packaging
+
+ # Add an automatically-generated changelog entry
+ # The --controlmaint takes the maintainer details from debian/control
+ ( cd "snap-confine-$pkg_version" && dch --controlmaint --newversion "${pkg_version}-1" "Automatic CI build")
+
+ # Build an unsigned source package
+ ( cd "snap-confine-$pkg_version" && dpkg-buildpackage -uc -us -S )
+
+ # Copy source package files to the top-level directory (this helps for
+ # interactive debugging since the package is available right there)
+ cp ./*.dsc ./*.debian.tar.* ./*.orig.tar.gz "$top_dir/"
+
+ # Ensure that we have a sbuild chroot ready
+ if ! schroot -l | grep "chroot:${distro_codename}-.*-sbuild"; then
+ sbuild-createchroot \
+ --include=eatmydata \
+ "--make-sbuild-tarball=/var/lib/sbuild/${distro_codename}-amd64.tar.gz" \
+ "$sbuild_createchroot_extra" \
+ "$distro_codename" "$(mktemp -d)" \
+ "$distro_archive"
+ fi
+
+ # Build a binary package in a clean chroot.
+ # NOTE: nocheck is because the package still includes old unit tests that
+ # are deeply integrated into how ubuntu apparmor denials are logged. This
+ # should be removed once those test are migrated to spread testes.
+ DEB_BUILD_OPTIONS=nocheck sbuild \
+ --arch-all \
+ --dist="$distro_codename" \
+ --batch \
+ "snap-confine_${pkg_version}-1.dsc"
+
+ # Copy all binary packages to the top-level directory
+ cp ./*.deb "$top_dir/"
+}
+
+
+# Apply tweaks
+case "$release_ID" in
+ ubuntu)
+ # apt update is hanging on security.ubuntu.com with IPv6.
+ sysctl -w net.ipv6.conf.all.disable_ipv6=1
+ trap "sysctl -w net.ipv6.conf.all.disable_ipv6=0" EXIT
+ ;;
+esac
+
+# Install all the build dependencies
+case "$release_ID" in
+ ubuntu|debian)
+ # treat APT_PROXY as a location of apt-cacher-ng to use
+ if [ -n "${APT_PROXY:-}" ]; then
+ printf 'Acquire::http::Proxy "%s";\n' "$APT_PROXY" > /etc/apt/apt.conf.d/00proxy
+ fi
+ # cope with unexpected /etc/apt/apt.conf.d/95cloud-init-proxy that may be in the image
+ rm -f /etc/apt/apt.conf.d/95cloud-init-proxy || :
+ # trusty support is under development right now
+ # we special-case the release until we have officially landed
+ if [ "$release_ID" = "ubuntu" ] && [ "$release_VERSION_ID" = "14.04" ]; then
+ add-apt-repository ppa:thomas-voss/trusty
+ fi
+ apt-get update
+ apt-get dist-upgrade -y
+ if [ "$release_ID" = "ubuntu" ] && [ "$release_VERSION_ID" = "14.04" ]; then
+ apt-get install -y systemd
+ # starting systemd manually is working around
+ # systemd not running as PID 1 on trusty systems.
+ service systemd start
+ fi
+ # On Debian and derivatives we need the following things:
+ # - sbuild -- to build the binary package with extra hygiene
+ # - devscripts -- to modify the changelog automatically
+ # - git -- to clone native downstream packaging
+ apt-get install --quiet -y sbuild devscripts git
+ # XXX: Taken from https://wiki.debian.org/sbuild
+ mkdir -p /root/.gnupg
+ # NOTE: We cannot use sbuild-update --keygen as virtual machines lack
+ # the necessary entropy to generate keys before the spread timeout
+ # kicks in. Instead we just copy pre-made, insecure keys from the
+ # source repository.
+ mkdir -p /var/lib/sbuild/apt-keys/
+ cp -a "$top_dir/spread-tests/data/apt-keys/"* /var/lib/sbuild/apt-keys/
+ sbuild-adduser "$LOGNAME"
+ ;;
+ *)
+ echo "unsupported distribution: $release_ID"
+ echo "patch spread-prepare to teach it about how to install build dependencies"
+ exit 1
+ ;;
+esac
+
+# Build and install the native package using downstream packaging and the fresh upstream tarball
+case "$release_ID" in
+ ubuntu|debian)
+ build_debian_or_ubuntu_package "$release_ID" "$release_VERSION_ID"
+ # Install the freshly-built packages
+ dpkg -i snap-confine_*.deb || apt-get -f install -y
+ dpkg -i ubuntu-core-launcher_*.deb || apt-get -f install -y
+ # Install snapd (testes require it)
+ apt-get install -y snapd
+ ;;
+ *)
+ echo "unsupported distribution: $release_ID"
+ exit 1
+ ;;
+esac
+
+# Install the core snap
+snap list | grep -q ubuntu-core || snap install ubuntu-core
--- /dev/null
+TESTS =
+
+all_tests = \
+ test_bad_seccomp_filter_args \
+ test_bad_seccomp_filter_args_clone \
+ test_bad_seccomp_filter_args_null \
+ test_bad_seccomp_filter_args_prctl \
+ test_bad_seccomp_filter_args_prio \
+ test_bad_seccomp_filter_args_socket \
+ test_bad_seccomp_filter_length \
+ test_bad_seccomp_filter_missing_trailing_newline \
+ test_complain \
+ test_complain_missed \
+ test_noprofile \
+ test_restrictions \
+ test_restrictions_working \
+ test_restrictions_working_args \
+ test_restrictions_working_args_clone \
+ test_restrictions_working_args_prctl \
+ test_restrictions_working_args_prio \
+ test_restrictions_working_args_socket \
+ test_unrestricted \
+ test_unrestricted_missed \
+ test_whitelist
+
+EXTRA_DIST = $(all_tests) common.sh
+
+if SECCOMP
+if CONFINEMENT_TESTS
+TESTS += $(all_tests)
+endif
+endif
+
+check: ../snap-confine
+
+.PHONY: check-syntax
+check-syntax:
+ shellcheck --format=gcc $(wildcard $(srcdir)/test_*) common.sh
--- /dev/null
+#!/bin/sh
+
+get_common_syscalls() {
+ cat <<EOF
+# filter that works ok for true
+
+open
+close
+
+mmap
+mmap2
+munmap
+mprotect
+
+fstat
+fstat64
+access
+read
+
+brk
+execve
+
+arch_prctl
+exit_group
+
+geteuid
+geteuid32
+getuid
+getuid32
+setresuid
+setresuid32
+setgid
+setgid32
+setuid
+setuid32
+
+set_thread_area
+EOF
+}
+
+L="$(pwd)/../snap-confine"
+export L
+
+TMP="$(mktemp -d)"
+trap 'rm -rf $TMP' EXIT
+
+export SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR="$TMP"
+export SNAPPY_LAUNCHER_INSIDE_TESTS="1"
+export SNAP_CONFINE_NO_ROOT=1
+export SNAP_NAME=name.app
+
+FAIL() {
+ printf ": FAIL\n"
+ exit 1
+}
+
+PASS() {
+ printf ": PASS\n"
+}
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+for i in 'bar' '-1' '0 - -1 0' '--10' '0:10' '1-10' '0,1' '0x0' 'a1' '1a' '1-' '1\\n1' '1\ 2' '999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999' ; do
+ printf "Test bad seccomp arg filtering (setpriority %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "setpriority $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
+
+# > SC_ARGS_MAXLENGTH in seccomp.c
+for i in '- - - - - - 7' '1 2 3 4 5 6 7' ; do
+ printf "Test bad seccomp arg filtering (too many args (> 6): %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "mbind $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ FAIL
+ else
+ PASS
+ fi
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+for i in 'CLONE_NEWNE' 'CLONE_NETNETT' 'CL0NE_NEWNET' ; do
+ printf "Test bad seccomp arg filtering (setns - %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "setns - $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+# Test these individually since you can't store \0 in a variable
+printf "Test bad seccomp arg filtering (socket S\\\0CK_STREAM)"
+cat "$TMP"/tmpl >"$TMP"/snap.name.app
+printf "socket S\0CK_STREAM\n" >>"$TMP"/snap.name.app
+
+if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+else
+ PASS
+fi
+
+# an embedded null that is after a valid arg stops processing of the arg
+# (limitation of fgets() implementation)
+printf "Test ok seccomp arg filtering (socket SOCK_STREAM\\\0bad stuff)"
+cat "$TMP"/tmpl >"$TMP"/snap.name.app
+printf "socket SOCK_STREAM\0bad stuff\n" >>"$TMP"/snap.name.app
+
+if $L snap.name.app /bin/true 2>/dev/null; then
+ PASS
+else
+ cat "$TMP"/snap.name.app
+ FAIL
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+for i in 'PR_GET_SECCOM' 'PR_GET_SECCOMPP' 'PR_GET_SECC0MP' ; do
+ printf "Test bad seccomp arg filtering (prctl %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "prctl $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
+
+for i in 'PR_CAP_AMBIENT_RAIS' 'PR_CAP_AMBIENT_RAISEE' ; do
+ printf "Test bad seccomp arg filtering (prctl PR_CAP_AMBIENT %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "prctl PR_CAP_AMBIENT $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+for i in 'PRIO_PROCES' 'PRIO_PROCESSS' 'PRIO_PR0CESS'; do
+ printf "Test bad seccomp arg filtering (setpriority %s 0 >= 0)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "setpriority $i 0 >=0" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+for i in 'AF_UNI' 'AF_UNIXX' 'AF_UN!X' ; do
+ printf "Test bad seccomp arg filtering (socket %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "socket $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
+
+for i in 'SOCK_STREA' 'SOCK_STREAMM' ; do
+ printf "Test bad seccomp arg filtering (socket - %s)" "$i"
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "socket - $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, bad arg test failed
+ cat "$TMP"/snap.name.app
+ FAIL
+ fi
+
+ # all good
+ PASS
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+printf "Test seccomp filter (bad - too long)"
+
+cat >"$TMP"/snap.name.app <<EOF
+# some comment
+baddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
+EOF
+
+if "$L" snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, length test failed
+ FAIL
+fi
+
+# all good
+PASS
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+printf "Test seccomp filter (bad - no trailing newline)"
+
+printf "missingnewline" > "$TMP"/snap.name.app
+
+if "$L" snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, length test failed
+ FAIL
+fi
+
+# all good
+PASS
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+cat >"$TMP/snap.name.app" <<EOF
+# some comment
+@complain
+EOF
+
+printf "Test that the @complain keyword works"
+if "$L" snap.name.app /bin/ls / >/dev/null; then
+ PASS
+else
+ FAIL
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+cat >"$TMP/snap.name.app" <<EOF
+# super strict filter
+@complai
+@complaim
+@omplain
+@COMPLAIN
+complain
+EOF
+
+# ensure that the command "true" can not run due to impossible
+# filtering
+
+printf "Test that near misses of complain fail"
+if "$L" snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, our filtering is broken!
+ FAIL
+else
+ # true returned a error code, check dmesg
+ if dmesg|tail -n1|grep -q "audit"; then
+ PASS
+ else
+ FAIL
+ fi
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+printf "Test that a non-existing profile causes the launcher to not start"
+if ! "$L" snap.name.app /bin/ls >"$TMP/testlog" 2>&1 ; then
+ PASS
+else
+ FAIL
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+cat >"$TMP/snap.name.app" <<EOF
+# super strict filter
+EOF
+
+# ensure that the command "true" can not run due to impossible
+# filtering
+
+printf "Test that seccomp filtering kills processes"
+if "$L" snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, our filtering is broken!
+ FAIL
+else
+ # true returned a error code, check dmesg
+ if dmesg|tail -n1|grep -q "audit"; then
+ PASS
+ else
+ FAIL
+ fi
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP/snap.name.app"
+cat >>"$TMP/snap.name.app" <<EOF
+# unknown syscalls are ignore
+i-dont-exit
+
+less-than-SC_MAX_LINE_LENGTH-but-still-looooooooooooooonnnnnnnngggggggggggggggg
+EOF
+
+# ensure that the command "true" can run with the right filter
+
+printf "Test that good whitelist"
+if "$L" snap.name.app /bin/true 2>/dev/null; then
+ PASS
+else
+ dmesg|tail -n1
+ FAIL
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+getpriority
+EOF
+
+for i in '- - 10' '- 0 10' '- 0 >=0' '- 0 >0' '- 0 <11' '- 0 <=10' ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "setpriority $i" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (setpriority %s)" "$i"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /usr/bin/nice -n 10 /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+done
+
+cat "$TMP"/tmpl >"$TMP"/snap.name.app
+{
+ echo "setpriority - - 10"
+ echo "setpriority - - <=9"
+ echo "setpriority - - >=11"
+} >>"$TMP"/snap.name.app
+
+printf "Test good seccomp arg filtering (cumulative setpriority)"
+# ensure that the command "true" can run with the right filter
+if $L snap.name.app /usr/bin/nice -n 10 /bin/true ; then
+ PASS
+else
+ dmesg|tail -n1
+ FAIL
+fi
+
+cat "$TMP"/tmpl >"$TMP"/snap.name.app
+echo "setpriority - - <=9" >>"$TMP"/snap.name.app
+echo "setpriority - - >=11" >>"$TMP"/snap.name.app
+
+printf "Test good seccomp arg filtering (cumulative setpriority blocks (ge/le))"
+if $L snap.name.app /usr/bin/nice -n 10 /bin/true 2>/dev/null ; then
+ FAIL
+else
+ PASS
+fi
+
+cat "$TMP"/tmpl >"$TMP"/snap.name.app
+echo "setpriority - - <10" >>"$TMP"/snap.name.app
+echo "setpriority - - >10" >>"$TMP"/snap.name.app
+
+printf "Test good seccomp arg filtering (cumulative setpriority blocks (gt/lt))"
+if $L snap.name.app /usr/bin/nice -n 10 /bin/true 2>/dev/null ; then
+ FAIL
+else
+ PASS
+fi
+
+# <= SC_ARGS_MAXLENGTH in seccomp.c
+for i in '1' '- 2' '- - 3' '- - - 4' '- - - - 5' '- - - - - 6' '1 2 3 4 5 6' ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "mbind $i" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (mbind %s)" "$i"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+done
+
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+# what we are testing
+EOF
+
+for i in CLONE_NEWIPC CLONE_NEWNET CLONE_NEWNS CLONE_NEWPID CLONE_NEWUSER CLONE_NEWUTS ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "setns - $i" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (setns - %s)" "$i"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+getpriority
+EOF
+
+for i in PR_CAP_AMBIENT PR_CAP_AMBIENT_RAISE PR_CAP_AMBIENT_LOWER PR_CAP_AMBIENT_IS_SET PR_CAP_AMBIENT_CLEAR_ALL PR_CAPBSET_READ PR_CAPBSET_DROP PR_SET_CHILD_SUBREAPER PR_GET_CHILD_SUBREAPER PR_SET_DUMPABLE PR_GET_DUMPABLE PR_SET_ENDIAN PR_GET_ENDIAN PR_SET_FPEMU PR_GET_FPEMU PR_SET_FPEXC PR_GET_FPEXC PR_SET_KEEPCAPS PR_GET_KEEPCAPS PR_MCE_KILL PR_MCE_KILL_GET PR_SET_MM PR_SET_MM_START_CODE PR_SET_MM_END_CODE PR_SET_MM_START_DATA PR_SET_MM_END_DATA PR_SET_MM_START_STACK PR_SET_MM_START_BRK PR_SET_MM_BRK PR_SET_MM_ARG_START PR_SET_MM_ARG_END PR_SET_MM_ENV_START PR_SET_MM_ENV_END PR_SET_MM_AUXV PR_SET_MM_EXE_FILE PR_MPX_ENABLE_MANAGEMENT PR_MPX_DISABLE_MANAGEMENT PR_SET_NAME PR_GET_NAME PR_SET_NO_NEW_PRIVS PR_GET_NO_NEW_PRIVS PR_SET_PDEATHSIG PR_GET_PDEATHSIG PR_SET_PTRACER PR_SET_SECCOMP PR_GET_SECCOMP PR_SET_SECUREBITS PR_GET_SECUREBITS PR_SET_THP_DISABLE PR_TASK_PERF_EVENTS_DISABLE PR_TASK_PERF_EVENTS_ENABLE PR_GET_THP_DISABLE PR_GET_TID_ADDRESS PR_SET_TIMERSLACK PR_GET_TIMERSLACK PR_SET_TIMING PR_GET_TIMING PR_SET_TSC PR_GET_TSC PR_SET_UNALIGN PR_GET_UNALIGN ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "prctl $i" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (prctl %s)" "$i"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+
+ if [ "$i" = "PR_CAP_AMBIENT" ]; then
+ for j in PR_CAP_AMBIENT_RAISE PR_CAP_AMBIENT_LOWER PR_CAP_AMBIENT_IS_SET PR_CAP_AMBIENT_CLEAR_ALL ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "prctl $i $j" >>"$TMP"/snap.name.app
+ printf "Test good seccomp arg filtering (prctl %s %s)" "$i" "$j"
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+ done
+ fi
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+getpriority
+EOF
+
+for i in PRIO_PROCESS PRIO_PGRP PRIO_USER ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "setpriority $i - -" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (setpriority %s - -)" "$i"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+get_common_syscalls >"$TMP"/tmpl
+cat >>"$TMP"/tmpl <<EOF
+getpriority
+EOF
+
+for i in AF_UNIX AF_LOCAL AF_INET AF_INET6 AF_IPX AF_NETLINK AF_X25 AF_AX25 AF_ATMPVC AF_APPLETALK AF_PACKET AF_ALG AF_CAN ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "socket $i" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (socket %s)" "$i"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+
+ for j in SOCK_STREAM SOCK_DGRAM SOCK_SEQPACKET SOCK_RAW SOCK_RDM SOCK_PACKET ; do
+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "socket $i $j" >>"$TMP"/snap.name.app
+
+ printf "Test good seccomp arg filtering (socket %s %s)" "$i" "$j"
+ # ensure that the command "true" can run with the right filter
+ if $L snap.name.app /bin/true ; then
+ PASS
+ else
+ dmesg|tail -n1
+ FAIL
+ fi
+ done
+done
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+cat >"$TMP/snap.name.app" <<EOF
+# some comment
+@unrestricted
+EOF
+
+printf "Test that the @unrestricted keyword works"
+if "$L" snap.name.app /bin/ls / >/dev/null; then
+ PASS
+else
+ FAIL
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+cat >"$TMP/snap.name.app" <<EOF
+# super strict filter
+@unrestricte
+@unrestrictes
+@nrestricted
+@UNRESTRICTED
+unrestricted
+EOF
+
+# ensure that the command "true" can not run due to impossible
+# filtering
+
+printf "Test that near misses of unrestricted fail"
+if "$L" snap.name.app /bin/true 2>/dev/null; then
+ # true returned successfully, our filtering is broken!
+ FAIL
+else
+ # true returned a error code, check dmesg
+ if dmesg|tail -n1|grep -q "audit"; then
+ PASS
+ else
+ FAIL
+ fi
+fi
--- /dev/null
+#!/bin/sh
+
+set -e
+
+. "${srcdir:-.}/common.sh"
+
+printf "Test appname whitelist"
+
+cat >"$TMP/snap.name.app" <<EOF
+# some comment
+@unrestricted
+EOF
+
+# good
+for name in snap.name.app snap.network-manager.NetworkManager snap.f00.bar-baz1 ; do
+ printf "Test good appname whitelist - '%s'" "$name"
+ if "$L" snap.name.app /bin/true ; then
+ PASS
+ else
+ FAIL
+ fi
+done
+
+for name in pkg-foo.bar.0binary-bar+baz pkg-foo_bar_1.1 appname/.. snap snap. snap.name. snap.name.app. snap!name.app snap.name!app sna.pname.app snap.n@me.app SNAP.name.app snap.Name.app snap.0name.app snap.-name.app snap.name.@app .name.app snap..name.app snap.name..app snap.name.app.. ; do
+ printf "Test bad appname whitelist - '%s'" "$name"
+ if "$L" $name /bin/true 2>/dev/null; then
+ FAIL
+ else
+ PASS
+ fi
+done
+
+printf "Test bad appname whitelist - 'appname space'"
+if "$L" 'appname space' /bin/true 2>/dev/null; then
+ # true returned successfully, our appname whitelist is broken!
+ FAIL
+else
+ PASS
+fi
--- /dev/null
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "config.h"
+#include "udev-support.h"
+
+#include <unistd.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <sched.h>
+#include <string.h>
+#include <linux/kdev_t.h>
+
+#include <ctype.h>
+
+#include "utils.h"
+#include "snap.h"
+
+void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path)
+{
+ if (udev_s == NULL)
+ die("snappy_udev is NULL");
+ if (udev_s->udev == NULL)
+ die("snappy_udev->udev is NULL");
+ if (udev_s->tagname_len == 0
+ || udev_s->tagname_len >= MAX_BUF
+ || strnlen(udev_s->tagname, MAX_BUF) != udev_s->tagname_len
+ || udev_s->tagname[udev_s->tagname_len] != '\0')
+ die("snappy_udev->tagname has invalid length");
+
+ debug("%s: %s %s", __func__, path, udev_s->tagname);
+
+ struct udev_device *d =
+ udev_device_new_from_syspath(udev_s->udev, path);
+ if (d == NULL)
+ die("can not find %s", path);
+ dev_t devnum = udev_device_get_devnum(d);
+ udev_device_unref(d);
+
+ int status = 0;
+ pid_t pid = fork();
+ if (pid < 0) {
+ die("could not fork");
+ }
+ if (pid == 0) {
+ uid_t real_uid, effective_uid, saved_uid;
+ if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0)
+ die("could not find user IDs");
+ // can't update the cgroup unless the real_uid is 0, euid as
+ // 0 is not enough
+ if (real_uid != 0 && effective_uid == 0)
+ if (setuid(0) != 0)
+ die("setuid failed");
+ char buf[64];
+ // pass snappy-add-dev an empty environment so the
+ // user-controlled environment can't be used to subvert
+ // snappy-add-dev
+ char *env[] = { NULL };
+ unsigned major = MAJOR(devnum);
+ unsigned minor = MINOR(devnum);
+ must_snprintf(buf, sizeof(buf), "%u:%u", major, minor);
+ execle("/lib/udev/snappy-app-dev", "/lib/udev/snappy-app-dev",
+ "add", udev_s->tagname, path, buf, NULL, env);
+ die("execl failed");
+ }
+ if (waitpid(pid, &status, 0) < 0)
+ die("waitpid failed");
+ if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
+ die("child exited with status %i", WEXITSTATUS(status));
+ else if (WIFSIGNALED(status))
+ die("child died with signal %i", WTERMSIG(status));
+}
+
+/*
+ * snappy_udev_init() - setup the snappy_udev structure. Return 0 if devices
+ * are assigned, else return -1. Callers should use snappy_udev_cleanup() to
+ * cleanup.
+ */
+int snappy_udev_init(const char *security_tag, struct snappy_udev *udev_s)
+{
+ debug("%s", __func__);
+ int rc = 0;
+
+ // extra paranoia
+ if (!verify_security_tag(security_tag))
+ die("security tag %s not allowed", security_tag);
+
+ udev_s->tagname[0] = '\0';
+ udev_s->tagname_len = 0;
+ // TAG+="snap_<security tag>" (udev doesn't like '.' in the tag name)
+ udev_s->tagname_len = must_snprintf(udev_s->tagname, MAX_BUF,
+ "%s", security_tag);
+ for (int i = 0; i < udev_s->tagname_len; i++)
+ if (udev_s->tagname[i] == '.')
+ udev_s->tagname[i] = '_';
+
+ udev_s->udev = udev_new();
+ if (udev_s->udev == NULL)
+ die("udev_new failed");
+
+ udev_s->devices = udev_enumerate_new(udev_s->udev);
+ if (udev_s->devices == NULL)
+ die("udev_enumerate_new failed");
+
+ if (udev_enumerate_add_match_tag(udev_s->devices, udev_s->tagname) != 0)
+ die("udev_enumerate_add_match_tag");
+
+ if (udev_enumerate_scan_devices(udev_s->devices) != 0)
+ die("udev_enumerate_scan failed");
+
+ udev_s->assigned = udev_enumerate_get_list_entry(udev_s->devices);
+ if (udev_s->assigned == NULL)
+ rc = -1;
+
+ return rc;
+}
+
+void snappy_udev_cleanup(struct snappy_udev *udev_s)
+{
+ // udev_s->assigned does not need to be unreferenced since it is a
+ // pointer into udev_s->devices
+ if (udev_s->devices != NULL)
+ udev_enumerate_unref(udev_s->devices);
+ if (udev_s->udev != NULL)
+ udev_unref(udev_s->udev);
+}
+
+void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s)
+{
+ debug("%s", __func__);
+ // Devices that must always be present
+ const char *static_devices[] = {
+ "/sys/class/mem/null",
+ "/sys/class/mem/full",
+ "/sys/class/mem/zero",
+ "/sys/class/mem/random",
+ "/sys/class/mem/urandom",
+ "/sys/class/tty/tty",
+ "/sys/class/tty/console",
+ "/sys/class/tty/ptmx",
+ NULL,
+ };
+
+ // extra paranoia
+ if (!verify_security_tag(security_tag))
+ die("security tag %s not allowed", security_tag);
+ if (udev_s == NULL)
+ die("snappy_udev is NULL");
+ if (udev_s->udev == NULL)
+ die("snappy_udev->udev is NULL");
+ if (udev_s->devices == NULL)
+ die("snappy_udev->devices is NULL");
+ if (udev_s->assigned == NULL)
+ die("snappy_udev->assigned is NULL");
+ if (udev_s->tagname_len == 0
+ || udev_s->tagname_len >= MAX_BUF
+ || strnlen(udev_s->tagname, MAX_BUF) != udev_s->tagname_len
+ || udev_s->tagname[udev_s->tagname_len] != '\0')
+ die("snappy_udev->tagname has invalid length");
+
+ // create devices cgroup controller
+ char cgroup_dir[PATH_MAX];
+
+ must_snprintf(cgroup_dir, sizeof(cgroup_dir),
+ "/sys/fs/cgroup/devices/%s/", security_tag);
+
+ if (mkdir(cgroup_dir, 0755) < 0 && errno != EEXIST)
+ die("mkdir failed");
+
+ // move ourselves into it
+ char cgroup_file[PATH_MAX];
+ must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir,
+ "tasks");
+
+ char buf[128];
+ must_snprintf(buf, sizeof(buf), "%i", getpid());
+ write_string_to_file(cgroup_file, buf);
+
+ // deny by default. Write 'a' to devices.deny to remove all existing
+ // devices that were added in previous launcher invocations, then add
+ // the static and assigned devices. This ensures that at application
+ // launch the cgroup only has what is currently assigned.
+ must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir,
+ "devices.deny");
+ write_string_to_file(cgroup_file, "a");
+
+ // add the common devices
+ for (int i = 0; static_devices[i] != NULL; i++)
+ run_snappy_app_dev_add(udev_s, static_devices[i]);
+
+ // add the assigned devices
+ while (udev_s->assigned != NULL) {
+ const char *path = udev_list_entry_get_name(udev_s->assigned);
+ if (path == NULL)
+ die("udev_list_entry_get_name failed");
+ run_snappy_app_dev_add(udev_s, path);
+ udev_s->assigned = udev_list_entry_get_next(udev_s->assigned);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_UDEV_SUPPORT_H
+#define SNAP_CONFINE_UDEV_SUPPORT_H
+
+#include <stddef.h>
+
+#include <libudev.h>
+
+#define MAX_BUF 1000
+
+struct snappy_udev {
+ struct udev *udev;
+ struct udev_enumerate *devices;
+ struct udev_list_entry *assigned;
+ char tagname[MAX_BUF];
+ size_t tagname_len;
+};
+
+void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path);
+int snappy_udev_init(const char *security_tag, struct snappy_udev *udev_s);
+void snappy_udev_cleanup(struct snappy_udev *udev_s);
+void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "unit-tests.h"
+
+int main(int argc, char **argv)
+{
+ return sc_run_unit_tests(&argc, &argv);
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "unit-tests.h"
+#include <glib.h>
+
+static void simple_test_case(void)
+{
+ g_assert(g_bit_storage(1) == 1);
+ g_assert_cmpint(g_bit_storage(1), ==, 1);
+}
+
+int sc_run_unit_tests(int *argc, char ***argv)
+{
+ g_test_init(argc, argv, NULL);
+ g_test_set_nonfatal_assertions();
+ g_test_add_func("/Simple Test Case", simple_test_case);
+ return g_test_run();
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_UNIT_TESTS_H
+#define SNAP_CONFINE_UNIT_TESTS_H
+
+/**
+ * Run unit tests and exit.
+ *
+ * The function inspects and modifies command line arguments.
+ * Internally it is using glib-test functions.
+ */
+int sc_run_unit_tests(int *argc, char ***argv);
+
+#endif // SNAP_CONFINE_SANITY_H
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "config.h"
+#include "user-support.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "utils.h"
+
+void setup_user_data()
+{
+ const char *user_data = getenv("SNAP_USER_DATA");
+
+ if (user_data == NULL)
+ return;
+
+ // Only support absolute paths.
+ if (user_data[0] != '/') {
+ die("user data directory must be an absolute path");
+ }
+
+ debug("creating user data directory: %s", user_data);
+ if (sc_nonfatal_mkpath(user_data, 0755) < 0) {
+ die("cannot create user data directory: %s", user_data);
+ };
+}
+
+void setup_user_xdg_runtime_dir()
+{
+ const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
+
+ if (xdg_runtime_dir == NULL)
+ return;
+ // Only support absolute paths.
+ if (xdg_runtime_dir[0] != '/') {
+ die("XDG_RUNTIME_DIR must be an absolute path");
+ }
+
+ errno = 0;
+ debug("creating user XDG_RUNTIME_DIR directory: %s", xdg_runtime_dir);
+ if (sc_nonfatal_mkpath(xdg_runtime_dir, 0755) < 0) {
+ die("cannot create user XDG_RUNTIME_DIR directory: %s",
+ xdg_runtime_dir);
+ }
+ // if successfully created the directory (ie, not EEXIST), then chmod it.
+ if (errno == 0 && chmod(xdg_runtime_dir, 0700) != 0) {
+ die("cannot change permissions of user XDG_RUNTIME_DIR directory to 0700");
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SNAP_CONFINE_USER_SUPPORT_H
+#define SNAP_CONFINE_USER_SUPPORT_H
+
+void setup_user_data();
+void setup_user_xdg_runtime_dir();
+void mkpath(const char *const path);
+
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "utils.h"
+#include "utils.c"
+
+#include <glib.h>
+
+static void test_str2bool()
+{
+ int err;
+ bool value;
+
+ err = str2bool("yes", &value);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_true(value);
+
+ err = str2bool("1", &value);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_true(value);
+
+ err = str2bool("no", &value);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_false(value);
+
+ err = str2bool("0", &value);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_false(value);
+
+ err = str2bool("", &value);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_false(value);
+
+ err = str2bool(NULL, &value);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_false(value);
+
+ err = str2bool("flower", &value);
+ g_assert_cmpint(err, ==, -1);
+ g_assert_cmpint(errno, ==, EINVAL);
+
+ err = str2bool("yes", NULL);
+ g_assert_cmpint(err, ==, -1);
+ g_assert_cmpint(errno, ==, EFAULT);
+}
+
+static void test_die()
+{
+ if (g_test_subprocess()) {
+ errno = 0;
+ die("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("death message\n");
+}
+
+static void test_die_with_errno()
+{
+ if (g_test_subprocess()) {
+ errno = EPERM;
+ die("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("death message: Operation not permitted\n");
+}
+
+/**
+ * Perform the rest of testing in a ephemeral directory.
+ *
+ * Create a temporary directory, move the current process there and undo those
+ * operations at the end of the test. If any additional directories or files
+ * are created in this directory they must be removed by the caller.
+ **/
+static void g_test_in_ephemeral_dir()
+{
+ gchar *temp_dir = g_dir_make_tmp(NULL, NULL);
+ gchar *orig_dir = g_get_current_dir();
+ int err = chdir(temp_dir);
+ g_assert_cmpint(err, ==, 0);
+
+ g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir);
+ g_test_queue_free(temp_dir);
+ g_test_queue_destroy((GDestroyNotify) chdir, orig_dir);
+ g_test_queue_free(orig_dir);
+}
+
+/**
+ * Test sc_nonfatal_mkpath() given two directories.
+ **/
+static void _test_sc_nonfatal_mkpath(const gchar * dirname,
+ const gchar * subdirname)
+{
+ // Check that directory does not exist.
+ g_assert_false(g_file_test(dirname, G_FILE_TEST_EXISTS |
+ G_FILE_TEST_IS_DIR));
+ // Use sc_nonfatal_mkpath to create the directory and ensure that it worked
+ // as expected.
+ g_test_queue_destroy((GDestroyNotify) rmdir, (char *)dirname);
+ int err = sc_nonfatal_mkpath(dirname, 0755);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_cmpint(errno, ==, 0);
+ g_assert_true(g_file_test(dirname, G_FILE_TEST_EXISTS |
+ G_FILE_TEST_IS_REGULAR));
+ // Use same function again to try to create the same directory and ensure
+ // that it didn't fail and properly retained EEXIST in errno.
+ err = sc_nonfatal_mkpath(dirname, 0755);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_cmpint(errno, ==, EEXIST);
+ // Now create a sub-directory of the original directory and observe the
+ // results. We should no longer see errno of EEXIST!
+ g_test_queue_destroy((GDestroyNotify) rmdir, (char *)subdirname);
+ err = sc_nonfatal_mkpath(subdirname, 0755);
+ g_assert_cmpint(err, ==, 0);
+ g_assert_cmpint(errno, ==, 0);
+}
+
+/**
+ * Test that sc_nonfatal_mkpath behaves when using relative paths.
+ **/
+static void test_sc_nonfatal_mkpath__relative()
+{
+ g_test_in_ephemeral_dir();
+ gchar *current_dir = g_get_current_dir();
+ g_test_queue_free(current_dir);
+ gchar *dirname = g_build_path("/", current_dir, "foo", NULL);
+ g_test_queue_free(dirname);
+ gchar *subdirname = g_build_path("/", current_dir, "foo", "bar", NULL);
+ g_test_queue_free(subdirname);
+ _test_sc_nonfatal_mkpath(dirname, subdirname);
+}
+
+/**
+ * Test that sc_nonfatal_mkpath behaves when using absolute paths.
+ **/
+static void test_sc_nonfatal_mkpath__absolute()
+{
+ g_test_in_ephemeral_dir();
+ const char *dirname = "foo";
+ const char *subdirname = "foo/bar";
+ _test_sc_nonfatal_mkpath(dirname, subdirname);
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/utils/str2bool", test_str2bool);
+ g_test_add_func("/utils/die", test_die);
+ g_test_add_func("/utils/die_with_errno", test_die_with_errno);
+ g_test_add_func("/utils/sc_nonfatal_mkpath/relative",
+ test_sc_nonfatal_mkpath__relative);
+ g_test_add_func("/utils/sc_nonfatal_mkpath/absolute",
+ test_sc_nonfatal_mkpath__absolute);
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "utils.h"
+#include "cleanup-funcs.h"
+
+void die(const char *msg, ...)
+{
+ int saved_errno = errno;
+ va_list va;
+ va_start(va, msg);
+ vfprintf(stderr, msg, va);
+ va_end(va);
+
+ if (errno != 0) {
+ fprintf(stderr, ": %s\n", strerror(saved_errno));
+ } else {
+ fprintf(stderr, "\n");
+ }
+ exit(1);
+}
+
+bool error(const char *msg, ...)
+{
+ va_list va;
+ va_start(va, msg);
+ vfprintf(stderr, msg, va);
+ va_end(va);
+
+ return false;
+}
+
+struct sc_bool_name {
+ const char *text;
+ bool value;
+};
+
+static const struct sc_bool_name sc_bool_names[] = {
+ {"yes", true},
+ {"no", false},
+ {"1", true},
+ {"0", false},
+ {"", false},
+};
+
+/**
+ * Convert string to a boolean value.
+ *
+ * The return value is 0 in case of success or -1 when the string cannot be
+ * converted correctly. In such case errno is set to indicate the problem and
+ * the value is not written back to the caller-supplied pointer.
+ **/
+static int str2bool(const char *text, bool * value)
+{
+ if (value == NULL) {
+ errno = EFAULT;
+ return -1;
+ }
+ if (text == NULL) {
+ *value = false;
+ return 0;
+ }
+ for (int i = 0; i < sizeof sc_bool_names / sizeof *sc_bool_names; ++i) {
+ if (strcmp(text, sc_bool_names[i].text) == 0) {
+ *value = sc_bool_names[i].value;
+ return 0;
+ }
+ }
+ errno = EINVAL;
+ return -1;
+}
+
+/**
+ * Get an environment variable and convert it to a boolean.
+ *
+ * Supported values are those of str2bool(), namely "yes", "no" as well as "1"
+ * and "0". All other values are treated as false and a diagnostic message is
+ * printed to stderr.
+ **/
+static bool getenv_bool(const char *name)
+{
+ const char *str_value = getenv(name);
+ bool value;
+ if (str2bool(str_value, &value) < 0) {
+ if (errno == EINVAL) {
+ fprintf(stderr,
+ "WARNING: unrecognized value of environment variable %s (expected yes/no or 1/0)\n",
+ name);
+ return false;
+ } else {
+ die("cannot convert value of environment variable %s to a boolean", name);
+ }
+ }
+ return value;
+}
+
+void debug(const char *msg, ...)
+{
+ if (getenv_bool("SNAP_CONFINE_DEBUG")) {
+ va_list va;
+ va_start(va, msg);
+ fprintf(stderr, "DEBUG: ");
+ vfprintf(stderr, msg, va);
+ fprintf(stderr, "\n");
+ va_end(va);
+ }
+}
+
+void write_string_to_file(const char *filepath, const char *buf)
+{
+ debug("write_string_to_file %s %s", filepath, buf);
+ FILE *f = fopen(filepath, "w");
+ if (f == NULL)
+ die("fopen %s failed", filepath);
+ if (fwrite(buf, strlen(buf), 1, f) != 1)
+ die("fwrite failed");
+ if (fflush(f) != 0)
+ die("fflush failed");
+ if (fclose(f) != 0)
+ die("fclose failed");
+}
+
+int must_snprintf(char *str, size_t size, const char *format, ...)
+{
+ int n;
+
+ va_list va;
+ va_start(va, format);
+ n = vsnprintf(str, size, format, va);
+ va_end(va);
+
+ if (n < 0 || n >= size)
+ die("failed to snprintf %s", str);
+
+ return n;
+}
+
+int sc_nonfatal_mkpath(const char *const path, mode_t mode)
+{
+ // If asked to create an empty path, return immediately.
+ if (strlen(path) == 0) {
+ return 0;
+ }
+ // We're going to use strtok_r, which needs to modify the path, so we'll
+ // make a copy of it.
+ char *path_copy __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
+ path_copy = strdup(path);
+ if (path_copy == NULL) {
+ return -1;
+ }
+ // Open flags to use while we walk the user data path:
+ // - Don't follow symlinks
+ // - Don't allow child access to file descriptor
+ // - Only open a directory (fail otherwise)
+ const int open_flags = O_NOFOLLOW | O_CLOEXEC | O_DIRECTORY;
+
+ // We're going to create each path segment via openat/mkdirat calls instead
+ // of mkdir calls, to avoid following symlinks and placing the user data
+ // directory somewhere we never intended for it to go. The first step is to
+ // get an initial file descriptor.
+ int fd __attribute__ ((cleanup(sc_cleanup_close))) = AT_FDCWD;
+ if (path_copy[0] == '/') {
+ fd = open("/", open_flags);
+ if (fd < 0) {
+ return -1;
+ }
+ }
+ // strtok_r needs a pointer to keep track of where it is in the string.
+ char *path_walker = NULL;
+
+ // Initialize tokenizer and obtain first path segment.
+ char *path_segment = strtok_r(path_copy, "/", &path_walker);
+ while (path_segment) {
+ // Try to create the directory. It's okay if it already existed, but
+ // return with error on any other error. Reset errno before attempting
+ // this as it may stay stale (errno is not reset if mkdirat(2) returns
+ // successfully).
+ errno = 0;
+ if (mkdirat(fd, path_segment, mode) < 0 && errno != EEXIST) {
+ return -1;
+ }
+ // Open the parent directory we just made (and close the previous one
+ // (but not the special value AT_FDCWD) so we can continue down the
+ // path.
+ int previous_fd = fd;
+ fd = openat(fd, path_segment, open_flags);
+ if (previous_fd != AT_FDCWD && close(previous_fd) != 0) {
+ return -1;
+ }
+ if (fd < 0) {
+ return -1;
+ }
+ // Obtain the next path segment.
+ path_segment = strtok_r(NULL, "/", &path_walker);
+ }
+ return 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include <stdlib.h>
+#include <stdbool.h>
+
+#ifndef CORE_LAUNCHER_UTILS_H
+#define CORE_LAUNCHER_UTILS_H
+
+__attribute__ ((noreturn))
+ __attribute__ ((format(printf, 1, 2)))
+void die(const char *fmt, ...);
+
+__attribute__ ((format(printf, 1, 2)))
+bool error(const char *fmt, ...);
+
+__attribute__ ((format(printf, 1, 2)))
+void debug(const char *fmt, ...);
+
+void write_string_to_file(const char *filepath, const char *buf);
+
+// snprintf version that dies on any error condition
+__attribute__ ((format(printf, 3, 4)))
+int must_snprintf(char *str, size_t size, const char *format, ...);
+
+/**
+ * Safely create a given directory.
+ *
+ * NOTE: non-fatal functions don't die on errors. It is the responsibility of
+ * the caller to call die() or handle the error appropriately.
+ *
+ * This function behaves like "mkdir -p" (recursive mkdir) with the exception
+ * that each directory is carefully created in a way that avoids symlink
+ * attacks. The preceding directory is kept openat(2) (along with O_DIRECTORY)
+ * and the next directory is created using mkdirat(2), this sequence continues
+ * while there are more directories to process.
+ *
+ * The function returns -1 in case of any error.
+ **/
+__attribute__ ((warn_unused_result))
+int sc_nonfatal_mkpath(const char *const path, mode_t mode);
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "snap.h"
+#include "snap.c"
+
+#include <glib.h>
+
+static void test_verify_security_tag()
+{
+ // First, test the names we know are good
+ g_assert_true(verify_security_tag("snap.name.app"));
+ g_assert_true(verify_security_tag
+ ("snap.network-manager.NetworkManager"));
+ g_assert_true(verify_security_tag("snap.f00.bar-baz1"));
+ g_assert_true(verify_security_tag("snap.foo.hook.bar"));
+ g_assert_true(verify_security_tag("snap.foo.hook.bar-baz"));
+
+ // Now, test the names we know are bad
+ g_assert_false(verify_security_tag("pkg-foo.bar.0binary-bar+baz"));
+ g_assert_false(verify_security_tag("pkg-foo_bar_1.1"));
+ g_assert_false(verify_security_tag("appname/.."));
+ g_assert_false(verify_security_tag("snap"));
+ g_assert_false(verify_security_tag("snap."));
+ g_assert_false(verify_security_tag("snap.name"));
+ g_assert_false(verify_security_tag("snap.name."));
+ g_assert_false(verify_security_tag("snap.name.app."));
+ g_assert_false(verify_security_tag("snap.name.hook."));
+ g_assert_false(verify_security_tag("snap!name.app"));
+ g_assert_false(verify_security_tag("snap.-name.app"));
+ g_assert_false(verify_security_tag("snap.name!app"));
+ g_assert_false(verify_security_tag("snap.name.-app"));
+ g_assert_false(verify_security_tag("snap.name.app!hook.foo"));
+ g_assert_false(verify_security_tag("snap.name.app.hook!foo"));
+ g_assert_false(verify_security_tag("snap.name.app.hook.-foo"));
+ g_assert_false(verify_security_tag("snap.name.app.hook.f00"));
+ g_assert_false(verify_security_tag("sna.pname.app"));
+ g_assert_false(verify_security_tag("snap.n@me.app"));
+ g_assert_false(verify_security_tag("SNAP.name.app"));
+ g_assert_false(verify_security_tag("snap.Name.app"));
+ g_assert_false(verify_security_tag("snap.0name.app"));
+ g_assert_false(verify_security_tag("snap.-name.app"));
+ g_assert_false(verify_security_tag("snap.name.@app"));
+ g_assert_false(verify_security_tag(".name.app"));
+ g_assert_false(verify_security_tag("snap..name.app"));
+ g_assert_false(verify_security_tag("snap.name..app"));
+ g_assert_false(verify_security_tag("snap.name.app.."));
+}
+
+static void __attribute__ ((constructor)) init()
+{
+ g_test_add_func("/snap/verify_security_tag", test_verify_security_tag);
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// for the tests
+var syscallExec = syscall.Exec
+
+// commandline args
+var opts struct {
+ Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"`
+ Hook string `long:"hook" description:"hook to run" hidden:"yes"`
+}
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Printf("cannot snap-exec: %s\n", err)
+ os.Exit(1)
+ }
+}
+
+func parseArgs(args []string) (app string, appArgs []string, err error) {
+ parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption)
+ rest, err := parser.ParseArgs(args)
+ if err != nil {
+ return "", nil, err
+ }
+ if len(rest) == 0 {
+ return "", nil, fmt.Errorf("need the application to run as argument")
+ }
+
+ // Catch some invalid parameter combinations, provide helpful errors
+ if opts.Hook != "" && opts.Command != "" {
+ return "", nil, fmt.Errorf("cannot use --hook and --command together")
+ }
+ if opts.Hook != "" && len(rest) > 1 {
+ return "", nil, fmt.Errorf("too many arguments for hook %q: %s", opts.Hook, strings.Join(rest, " "))
+ }
+
+ return rest[0], rest[1:], nil
+}
+
+func run() error {
+ snapApp, extraArgs, err := parseArgs(os.Args[1:])
+ if err != nil {
+ return err
+ }
+
+ // the SNAP_REVISION is set by `snap run` - we can not (easily)
+ // find it in `snap-exec` because `snap-exec` is run inside the
+ // confinement and (generally) can not talk to snapd
+ revision := os.Getenv("SNAP_REVISION")
+
+ // Now actually handle the dispatching
+ if opts.Hook != "" {
+ return snapExecHook(snapApp, revision, opts.Hook)
+ }
+
+ return snapExecApp(snapApp, revision, opts.Command, extraArgs)
+}
+
+func findCommand(app *snap.AppInfo, command string) (string, error) {
+ var cmd string
+ switch command {
+ case "shell":
+ cmd = "/bin/bash"
+ case "stop":
+ cmd = app.StopCommand
+ case "post-stop":
+ cmd = app.PostStopCommand
+ case "":
+ cmd = app.Command
+ default:
+ return "", fmt.Errorf("cannot use %q command", command)
+ }
+
+ if cmd == "" {
+ return "", fmt.Errorf("no %q command found for %q", command, app.Name)
+ }
+ return cmd, nil
+}
+
+func snapExecApp(snapApp, revision, command string, args []string) error {
+ rev, err := snap.ParseRevision(revision)
+ if err != nil {
+ return fmt.Errorf("cannot parse revision %q: %s", revision, err)
+ }
+
+ snapName, appName := snap.SplitSnapApp(snapApp)
+ info, err := snap.ReadInfo(snapName, &snap.SideInfo{
+ Revision: rev,
+ })
+ if err != nil {
+ return fmt.Errorf("cannot read info for %q: %s", snapName, err)
+ }
+
+ app := info.Apps[appName]
+ if app == nil {
+ return fmt.Errorf("cannot find app %q in %q", appName, snapName)
+ }
+
+ cmdAndArgs, err := findCommand(app, command)
+ if err != nil {
+ return err
+ }
+ // strings.Split() is ok here because we validate all app fields
+ // and the whitelist is pretty strict (see
+ // snap/validate.go:appContentWhitelist)
+ cmdArgv := strings.Split(cmdAndArgs, " ")
+ cmd := cmdArgv[0]
+ cmdArgs := cmdArgv[1:]
+
+ // build the environment from the yaml
+ env := append(os.Environ(), app.Env()...)
+
+ // run the command
+ fullCmd := filepath.Join(app.Snap.MountDir(), cmd)
+ if command == "shell" {
+ fullCmd = "/bin/bash"
+ cmdArgs = nil
+ }
+ fullCmdArgs := []string{fullCmd}
+ fullCmdArgs = append(fullCmdArgs, cmdArgs...)
+ fullCmdArgs = append(fullCmdArgs, args...)
+ if err := syscallExec(fullCmd, fullCmdArgs, env); err != nil {
+ return fmt.Errorf("cannot exec %q: %s", fullCmd, err)
+ }
+ // this is never reached except in tests
+ return nil
+}
+
+func snapExecHook(snapName, revision, hookName string) error {
+ rev, err := snap.ParseRevision(revision)
+ if err != nil {
+ return err
+ }
+
+ info, err := snap.ReadInfo(snapName, &snap.SideInfo{
+ Revision: rev,
+ })
+ if err != nil {
+ return err
+ }
+
+ hook := info.Hooks[hookName]
+ if hook == nil {
+ return fmt.Errorf("cannot find hook %q in %q", hookName, snapName)
+ }
+
+ // build the environment
+ env := append(os.Environ(), hook.Env()...)
+
+ // run the hook
+ hookPath := filepath.Join(hook.Snap.HooksDir(), hook.Name)
+ return syscallExec(hookPath, []string{hookPath}, env)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "syscall"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type snapExecSuite struct{}
+
+var _ = Suite(&snapExecSuite{})
+
+func (s *snapExecSuite) SetUpTest(c *C) {
+ // clean previous parse runs
+ opts.Command = ""
+ opts.Hook = ""
+}
+
+func (s *snapExecSuite) TearDown(c *C) {
+ syscallExec = syscall.Exec
+ dirs.SetRootDir("/")
+}
+
+var mockYaml = []byte(`name: snapname
+version: 1.0
+apps:
+ app:
+ command: run-app cmd-arg1
+ stop-command: stop-app
+ post-stop-command: post-stop-app
+ environment:
+ LD_LIBRARY_PATH: /some/path
+ nostop:
+ command: nostop
+`)
+
+var mockHookYaml = []byte(`name: snapname
+version: 1.0
+hooks:
+ configure:
+`)
+
+var mockContents = ""
+
+var binaryTemplate = `#!/bin/sh
+echo "$(basename $0)" >> %[1]q
+for arg in "$@"; do
+echo "$arg" >> %[1]q
+done
+printf "\n" >> %[1]q`
+
+func (s *snapExecSuite) TestInvalidCombinedParameters(c *C) {
+ invalidParameters := []string{"--hook=hook-name", "--command=command-name", "snap-name"}
+ _, _, err := parseArgs(invalidParameters)
+ c.Check(err, ErrorMatches, ".*cannot use --hook and --command together.*")
+}
+
+func (s *snapExecSuite) TestInvalidExtraParameters(c *C) {
+ invalidParameters := []string{"--hook=hook-name", "snap-name", "foo", "bar"}
+ _, _, err := parseArgs(invalidParameters)
+ c.Check(err, ErrorMatches, ".*too many arguments for hook \"hook-name\": snap-name foo bar.*")
+}
+
+func (s *snapExecSuite) TestFindCommand(c *C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, IsNil)
+
+ for _, t := range []struct {
+ cmd string
+ expected string
+ }{
+ {cmd: "", expected: `run-app cmd-arg1`},
+ {cmd: "stop", expected: "stop-app"},
+ {cmd: "post-stop", expected: "post-stop-app"},
+ } {
+ cmd, err := findCommand(info.Apps["app"], t.cmd)
+ c.Check(err, IsNil)
+ c.Check(cmd, Equals, t.expected)
+ }
+}
+
+func (s *snapExecSuite) TestFindCommandInvalidCommand(c *C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, IsNil)
+
+ _, err = findCommand(info.Apps["app"], "xxx")
+ c.Check(err, ErrorMatches, `cannot use "xxx" command`)
+}
+
+func (s *snapExecSuite) TestFindCommandNoCommand(c *C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, IsNil)
+
+ _, err = findCommand(info.Apps["nostop"], "stop")
+ c.Check(err, ErrorMatches, `no "stop" command found for "nostop"`)
+}
+
+func (s *snapExecSuite) TestSnapExecAppIntegration(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("42"),
+ })
+
+ execArgv0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ syscallExec = func(argv0 string, argv []string, env []string) error {
+ execArgv0 = argv0
+ execArgs = argv
+ execEnv = env
+ return nil
+ }
+
+ // launch and verify its run the right way
+ err := snapExecApp("snapname.app", "42", "stop", []string{"arg1", "arg2"})
+ c.Assert(err, IsNil)
+ c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/stop-app", dirs.SnapMountDir))
+ c.Check(execArgs, DeepEquals, []string{execArgv0, "arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n")
+}
+
+func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("42"),
+ })
+
+ execArgv0 := ""
+ execArgs := []string{}
+ syscallExec = func(argv0 string, argv []string, env []string) error {
+ execArgv0 = argv0
+ execArgs = argv
+ return nil
+ }
+
+ // launch and verify it ran correctly
+ err := snapExecHook("snapname", "42", "configure")
+ c.Assert(err, IsNil)
+ c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/meta/hooks/configure", dirs.SnapMountDir))
+ c.Check(execArgs, DeepEquals, []string{execArgv0})
+}
+
+func (s *snapExecSuite) TestSnapExecHookMissingHookIntegration(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("42"),
+ })
+
+ err := snapExecHook("snapname", "42", "missing-hook")
+ c.Assert(err, NotNil)
+ c.Assert(err, ErrorMatches, "cannot find hook \"missing-hook\" in \"snapname\"")
+}
+
+func (s *snapExecSuite) TestSnapExecIgnoresUnknownArgs(c *C) {
+ snapApp, rest, err := parseArgs([]string{"--command=shell", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, IsNil)
+ c.Assert(opts.Command, Equals, "shell")
+ c.Assert(snapApp, DeepEquals, "snapname.app")
+ c.Assert(rest, DeepEquals, []string{"--arg1", "arg2"})
+}
+
+func (s *snapExecSuite) TestSnapExecErrorsOnUnknown(c *C) {
+ _, _, err := parseArgs([]string{"--command=shell", "--unknown", "snapname.app", "--arg1", "arg2"})
+ c.Check(err, ErrorMatches, "unknown flag `unknown'")
+}
+
+func (s *snapExecSuite) TestSnapExecErrorsOnMissingSnapApp(c *C) {
+ _, _, err := parseArgs([]string{"--command=shell"})
+ c.Check(err, ErrorMatches, "need the application to run as argument")
+}
+
+func (s *snapExecSuite) TestSnapExecAppRealIntegration(c *C) {
+ // we need a lot of mocks
+ dirs.SetRootDir(c.MkDir())
+
+ oldOsArgs := os.Args
+ defer func() { os.Args = oldOsArgs }()
+
+ os.Setenv("SNAP_REVISION", "42")
+ defer os.Unsetenv("SNAP_REVISION")
+
+ snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("42"),
+ })
+
+ canaryFile := filepath.Join(c.MkDir(), "canary.txt")
+ script := fmt.Sprintf("%s/snapname/42/run-app", dirs.SnapMountDir)
+ err := ioutil.WriteFile(script, []byte(fmt.Sprintf(binaryTemplate, canaryFile)), 0755)
+ c.Assert(err, IsNil)
+
+ // we can not use the real syscall.execv here because it would
+ // replace the entire test :)
+ syscallExec = actuallyExec
+
+ // run it
+ os.Args = []string{"snap-exec", "snapname.app", "foo", "--bar=baz", "foobar"}
+ err = run()
+ c.Assert(err, IsNil)
+
+ output, err := ioutil.ReadFile(canaryFile)
+ c.Assert(err, IsNil)
+ c.Assert(string(output), Equals, `run-app
+cmd-arg1
+foo
+--bar=baz
+foobar
+
+`)
+}
+
+func (s *snapExecSuite) TestSnapExecHookRealIntegration(c *C) {
+ // we need a lot of mocks
+ dirs.SetRootDir(c.MkDir())
+
+ oldOsArgs := os.Args
+ defer func() { os.Args = oldOsArgs }()
+
+ os.Setenv("SNAP_REVISION", "42")
+ defer os.Unsetenv("SNAP_REVISION")
+
+ canaryFile := filepath.Join(c.MkDir(), "canary.txt")
+
+ testSnap := snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("42"),
+ })
+ hookPath := filepath.Join("meta", "hooks", "configure")
+ hookPathAndContents := []string{hookPath, fmt.Sprintf(binaryTemplate, canaryFile)}
+ snaptest.PopulateDir(testSnap.MountDir(), [][]string{hookPathAndContents})
+ hookPath = filepath.Join(testSnap.MountDir(), hookPath)
+ c.Assert(os.Chmod(hookPath, 0755), IsNil)
+
+ // we can not use the real syscall.execv here because it would
+ // replace the entire test :)
+ syscallExec = actuallyExec
+
+ // run it
+ os.Args = []string{"snap-exec", "--hook=configure", "snapname"}
+ err := run()
+ c.Assert(err, IsNil)
+
+ output, err := ioutil.ReadFile(canaryFile)
+ c.Assert(err, IsNil)
+ c.Assert(string(output), Equals, "configure\n\n")
+}
+
+func actuallyExec(argv0 string, argv []string, env []string) error {
+ cmd := exec.Command(argv[0], argv[1:]...)
+ cmd.Env = env
+ output, err := cmd.CombinedOutput()
+ if len(output) > 0 {
+ return fmt.Errorf("Expected output length to be 0, it was %d", len(output))
+ }
+ return err
+}
+
+func (s *snapExecSuite) TestSnapExecShellIntegration(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("42"),
+ })
+
+ execArgv0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ syscallExec = func(argv0 string, argv []string, env []string) error {
+ execArgv0 = argv0
+ execArgs = argv
+ execEnv = env
+ return nil
+ }
+
+ // launch and verify its run the right way
+ err := snapExecApp("snapname.app", "42", "shell", []string{"-c", "echo foo"})
+ c.Assert(err, IsNil)
+ c.Check(execArgv0, Equals, "/bin/bash")
+ c.Check(execArgs, DeepEquals, []string{execArgv0, "-c", "echo foo"})
+ c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdAbort struct {
+ Positional struct {
+ ID changeID
+ } `positional-args:"yes" required:"yes"`
+}
+
+var shortAbortHelp = i18n.G("Abort a pending change")
+
+var longAbortHelp = i18n.G(`
+The abort command attempts to abort a change that still has pending tasks.
+`)
+
+func init() {
+ addCommand("abort",
+ shortAbortHelp,
+ longAbortHelp,
+ func() flags.Commander {
+ return &cmdAbort{}
+ },
+ nil,
+ []argDesc{{name: i18n.G("<change-id>")}},
+ )
+}
+
+func (x *cmdAbort) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ cli := Client()
+ _, err := cli.Abort(string(x.Positional.ID))
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdAck struct {
+ AckOptions struct {
+ AssertionFile flags.Filename
+ } `positional-args:"true" required:"true"`
+}
+
+var shortAckHelp = i18n.G("Adds an assertion to the system")
+var longAckHelp = i18n.G(`
+The ack command tries to add an assertion to the system assertion database.
+
+The assertion may also be a newer revision of a preexisting assertion that it
+will replace.
+
+To succeed the assertion must be valid, its signature verified with a known
+public key and the assertion consistent with and its prerequisite in the
+database.
+`)
+
+func init() {
+ addCommand("ack", shortAckHelp, longAckHelp, func() flags.Commander {
+ return &cmdAck{}
+ }, nil, []argDesc{{
+ name: i18n.G("<assertion file>"),
+ desc: i18n.G("Assertion file"),
+ }})
+}
+
+func ackFile(assertFile string) error {
+ assertData, err := ioutil.ReadFile(assertFile)
+ if err != nil {
+ return err
+ }
+
+ return Client().Ack(assertData)
+}
+
+func (x *cmdAck) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ if err := ackFile(string(x.AckOptions.AssertionFile)); err != nil {
+ return fmt.Errorf("cannot assert: %v", err)
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdAlias struct {
+ Reset bool `long:"reset"`
+
+ Positionals struct {
+ Snap installedSnapName `required:"yes"`
+ Aliases []string `required:"yes"`
+ } `positional-args:"true"`
+}
+
+// TODO: implement a Completer for aliases
+
+var shortAliasHelp = i18n.G("Enables the given aliases")
+var longAliasHelp = i18n.G(`
+The alias command enables the given application aliases defined by the snap.
+
+Once enabled the respective application commands can be invoked just using the aliases.
+`)
+
+func init() {
+ addCommand("alias", shortAliasHelp, longAliasHelp, func() flags.Commander {
+ return &cmdAlias{}
+ }, map[string]string{
+ "reset": i18n.G("Reset the aliases to their default state, enabled for automatic aliases, disabled otherwise"),
+ }, []argDesc{
+ {name: "<snap>"},
+ {name: i18n.G("<alias>")},
+ })
+}
+
+func (x *cmdAlias) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ snapName := string(x.Positionals.Snap)
+ aliases := x.Positionals.Aliases
+
+ cli := Client()
+ op := cli.Alias
+ if x.Reset {
+ op = cli.ResetAliases
+ }
+ id, err := op(snapName, aliases)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, id)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestAliasHelp(c *C) {
+ msg := `Usage:
+ snap.test [OPTIONS] alias [alias-OPTIONS] [<snap>] [<alias>...]
+
+The alias command enables the given application aliases defined by the snap.
+
+Once enabled the respective application commands can be invoked just using the
+aliases.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+
+[alias command options]
+ --reset Reset the aliases to their default state, enabled for
+ automatic aliases, disabled otherwise
+`
+ rest, err := Parser().ParseArgs([]string{"alias", "--help"})
+ c.Assert(err.Error(), Equals, msg)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestAlias(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/aliases":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "alias",
+ "snap": "alias-snap",
+ "aliases": []interface{}{"alias1", "alias2"},
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"alias", "alias-snap", "alias1", "alias2"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestAliasReset(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/aliases":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "reset",
+ "snap": "alias-snap",
+ "aliases": []interface{}{"alias1", "alias2"},
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"alias", "--reset", "alias-snap", "alias1", "alias2"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdAliases struct {
+ Positionals struct {
+ Snap installedSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"true"`
+}
+
+var shortAliasesHelp = i18n.G("Lists aliases in the system")
+var longAliasesHelp = i18n.G(`
+The aliases command lists all aliases available in the system and their status.
+
+$ snap aliases <snap>
+
+Lists only the aliases defined by the specified snap.
+
+An alias noted as undefined means it was explicitly enabled or disabled but is
+not defined in the current revision of the snap; possibly temporarely (e.g
+because of a revert), if not this can be cleared with snap alias --reset.
+`)
+
+func init() {
+ addCommand("aliases", shortAliasesHelp, longAliasesHelp, func() flags.Commander {
+ return &cmdAliases{}
+ }, nil, nil)
+}
+
+type aliasInfo struct {
+ Snap string
+ App string
+ Alias string
+ Status string
+}
+
+type aliasInfos []*aliasInfo
+
+func (infos aliasInfos) Len() int { return len(infos) }
+func (infos aliasInfos) Swap(i, j int) { infos[i], infos[j] = infos[j], infos[i] }
+func (infos aliasInfos) Less(i, j int) bool {
+ if infos[i].Snap < infos[j].Snap {
+ return true
+ }
+ if infos[i].Snap == infos[j].Snap {
+ if infos[i].App != "" {
+ if infos[j].App == "" {
+ return true
+ }
+ if infos[i].App < infos[j].App {
+ return true
+ }
+ }
+ if infos[i].App == infos[j].App {
+ if infos[i].Alias < infos[j].Alias {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (x *cmdAliases) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ allStatuses, err := Client().Aliases()
+ if err == nil {
+ w := tabWriter()
+ fmt.Fprintln(w, i18n.G("App\tAlias\tNotes"))
+ defer w.Flush()
+ var infos aliasInfos
+ filterSnap := string(x.Positionals.Snap)
+ if filterSnap != "" {
+ allStatuses = map[string]map[string]client.AliasStatus{
+ filterSnap: allStatuses[filterSnap],
+ }
+ }
+ for snapName, aliasStatuses := range allStatuses {
+ for alias, aliasStatus := range aliasStatuses {
+ infos = append(infos, &aliasInfo{
+ Snap: snapName,
+ App: aliasStatus.App,
+ Alias: alias,
+ Status: aliasStatus.Status,
+ })
+ }
+ }
+ sort.Sort(infos)
+
+ for _, info := range infos {
+ var notes []string
+ app := info.App
+ if app == "" {
+ app = fmt.Sprintf("%s.???", info.Snap)
+ notes = append(notes, "undefined")
+ }
+ if info.Status != "" {
+ notes = append(notes, info.Status)
+ }
+ notesStr := strings.Join(notes, ",")
+ if notesStr == "" {
+ notesStr = "-"
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\n", app, info.Alias, notesStr)
+ }
+ }
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "io/ioutil"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestAliasesHelp(c *C) {
+ msg := `Usage:
+ snap.test [OPTIONS] aliases [<snap>]
+
+The aliases command lists all aliases available in the system and their status.
+
+$ snap aliases <snap>
+
+Lists only the aliases defined by the specified snap.
+
+An alias noted as undefined means it was explicitly enabled or disabled but is
+not defined in the current revision of the snap; possibly temporarely (e.g
+because of a revert), if not this can be cleared with snap alias --reset.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+`
+ rest, err := Parser().ParseArgs([]string{"aliases", "--help"})
+ c.Assert(err.Error(), Equals, msg)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestAliases(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/aliases")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": map[string]map[string]client.AliasStatus{
+ "foo": {
+ "foo0": {App: "foo", Status: "auto"},
+ "foo_reset": {App: "foo.reset", Status: ""},
+ },
+ "bar": {
+ "bar_dump": {App: "bar.dump", Status: "enabled"},
+ "bar_dump.1": {App: "", Status: "disabled"},
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"aliases"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "App Alias Notes\n" +
+ "bar.dump bar_dump enabled\n" +
+ "bar.??? bar_dump.1 undefined,disabled\n" +
+ "foo foo0 auto\n" +
+ "foo.reset foo_reset -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestAliasesFilterSnap(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/aliases")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": map[string]map[string]client.AliasStatus{
+ "foo": {
+ "foo0": {App: "foo", Status: "auto"},
+ "foo_reset": {App: "foo.reset", Status: ""},
+ },
+ "bar": {
+ "bar_dump": {App: "bar.dump", Status: "enabled"},
+ "bar_dump.1": {App: "", Status: "disabled"},
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"aliases", "foo"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "App Alias Notes\n" +
+ "foo foo0 auto\n" +
+ "foo.reset foo_reset -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestAliasesNone(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/aliases")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": map[string]map[string]client.AliasStatus{},
+ })
+ })
+ _, err := Parser().ParseArgs([]string{"aliases"})
+ c.Assert(err, IsNil)
+ expectedStdout := "" +
+ "App Alias Notes\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestAliasesSorting(c *C) {
+ tests := []struct {
+ snap1 string
+ app1 string
+ alias1 string
+ snap2 string
+ app2 string
+ alias2 string
+ }{
+ {"bar", "bar", "r", "baz", "baz", "z"},
+ {"bar", "bar", "bar0", "bar", "bar.app", "bapp"},
+ {"bar", "bar.app1", "bapp1", "bar", "bar.app2", "bapp2"},
+ {"bar", "bar", "bar0", "bar", "", "bapp"},
+ {"bar", "bar.app1", "appy", "bar", "", "appx"},
+ {"bar", "", "bapp1", "bar", "", "bapp2"},
+ {"bar", "bar.app1", "appx", "bar", "bar.app1", "appy"},
+ }
+
+ for _, test := range tests {
+ res := AliasInfoLess(test.snap1, test.alias1, test.app1, test.snap2, test.alias2, test.app2)
+ c.Check(res, Equals, true, Commentf("%v", test))
+
+ rres := AliasInfoLess(test.snap2, test.alias2, test.app2, test.snap1, test.alias1, test.app1)
+ c.Check(rres, Equals, false, Commentf("reversed %v", test))
+ }
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "bufio"
+ "crypto"
+ "encoding/base64"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "syscall"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/release"
+)
+
+const autoImportsName = "auto-import.assert"
+
+var mountInfoPath = "/proc/self/mountinfo"
+
+func autoImportCandidates() ([]string, error) {
+ var cands []string
+
+ // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt,
+ // sec. 3.5
+ f, err := os.Open(mountInfoPath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ l := strings.Fields(scanner.Text())
+
+ // Per proc.txt:3.5, /proc/<pid>/mountinfo looks like
+ //
+ // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
+ // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
+ //
+ // and (7) has zero or more elements, find the "-" separator.
+ i := 6
+ for i < len(l) && l[i] != "-" {
+ i++
+ }
+ if i+2 >= len(l) {
+ continue
+ }
+
+ mountSrc := l[i+2]
+
+ // skip everything that is not a device (cgroups, debugfs etc)
+ if !strings.HasPrefix(mountSrc, "/dev/") {
+ continue
+ }
+ // skip all loop devices (snaps)
+ if strings.HasPrefix(mountSrc, "/dev/loop") {
+ continue
+ }
+
+ mountPoint := l[4]
+ cand := filepath.Join(mountPoint, autoImportsName)
+ if osutil.FileExists(cand) {
+ cands = append(cands, cand)
+ }
+ }
+
+ return cands, scanner.Err()
+
+}
+
+func queueFile(src string) error {
+ // refuse huge files, this is for assertions
+ fi, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+ // 640kb ought be to enough for anyone
+ if fi.Size() > 640*1024 {
+ msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size())
+ logger.Noticef("error: %v", msg)
+ return msg
+ }
+
+ // ensure name is predictable, weak hash is ok
+ hash, _, err := osutil.FileDigest(src, crypto.SHA3_384)
+ if err != nil {
+ return err
+ }
+
+ dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash)))
+ if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ return err
+ }
+
+ return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite)
+}
+
+func autoImportFromSpool() (added int, err error) {
+ files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir)
+ if os.IsNotExist(err) {
+ return 0, nil
+ }
+ if err != nil {
+ return 0, err
+ }
+
+ for _, fi := range files {
+ cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name())
+ if err := ackFile(cand); err != nil {
+ logger.Noticef("error: cannot import %s: %s", cand, err)
+ continue
+ } else {
+ logger.Noticef("imported %s", cand)
+ added++
+ }
+ // FIXME: only remove stuff older than N days?
+ if err := os.Remove(cand); err != nil {
+ return 0, err
+ }
+ }
+
+ return added, nil
+}
+
+func autoImportFromAllMounts() (int, error) {
+ cands, err := autoImportCandidates()
+ if err != nil {
+ return 0, err
+ }
+
+ added := 0
+ for _, cand := range cands {
+ err := ackFile(cand)
+ // the server is not ready yet
+ if _, ok := err.(client.ConnectionError); ok {
+ logger.Noticef("queuing for later %s", cand)
+ if err := queueFile(cand); err != nil {
+ return 0, err
+ }
+ continue
+ }
+ if err != nil {
+ logger.Noticef("error: cannot import %s: %s", cand, err)
+ continue
+ } else {
+ logger.Noticef("imported %s", cand)
+ }
+ added++
+ }
+
+ return added, nil
+}
+
+func tryMount(deviceName string) (string, error) {
+ tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-")
+ if err != nil {
+ err = fmt.Errorf("cannot create temporary mount point: %v", err)
+ logger.Noticef("error: %v", err)
+ return "", err
+ }
+ // udev does not provide much environment ;)
+ if os.Getenv("PATH") == "" {
+ os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin")
+ }
+ // not using syscall.Mount() because we don't know the fs type in advance
+ cmd := exec.Command("mount", "-o", "ro", "--make-private", deviceName, tmpMountTarget)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ os.Remove(tmpMountTarget)
+ err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err))
+ logger.Noticef("error: %v", err)
+ return "", err
+ }
+
+ return tmpMountTarget, nil
+}
+
+func doUmount(mp string) error {
+ if err := syscall.Unmount(mp, 0); err != nil {
+ return err
+ }
+ return os.Remove(mp)
+}
+
+type cmdAutoImport struct {
+ Mount []string `long:"mount" arg-name:"<device path>"`
+
+ ForceClassic bool `long:"force-classic"`
+}
+
+var shortAutoImportHelp = i18n.G("Inspects devices for actionable information")
+
+var longAutoImportHelp = i18n.G(`
+The auto-import command searches available mounted devices looking for
+assertions that are signed by trusted authorities, and potentially
+performs system changes based on them.
+
+If one or more device paths are provided via --mount, these are temporariy
+mounted to be inspected as well. Even in that case the command will still
+consider all available mounted devices for inspection.
+
+Imported assertions must be made available in the auto-import.assert file
+in the root of the filesystem.
+`)
+
+func init() {
+ cmd := addCommand("auto-import",
+ shortAutoImportHelp,
+ longAutoImportHelp,
+ func() flags.Commander {
+ return &cmdAutoImport{}
+ }, map[string]string{
+ "mount": i18n.G("Temporarily mount device before inspecting"),
+ "force-classic": i18n.G("Force import on classic systems"),
+ }, nil)
+ cmd.hidden = true
+}
+
+func autoAddUsers() error {
+ cmd := cmdCreateUser{
+ Known: true, Sudoer: true,
+ }
+ return cmd.Execute(nil)
+}
+
+func (x *cmdAutoImport) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ if release.OnClassic && !x.ForceClassic {
+ fmt.Fprintf(Stderr, "auto-import is disabled on classic\n")
+ return nil
+ }
+
+ for _, path := range x.Mount {
+ // udev adds new /dev/loopX devices on the fly when a
+ // loop mount happens and there is no loop device left.
+ //
+ // We need to ignore these events because otherwise both
+ // our mount and the "mount -o loop" fight over the same
+ // device and we get nasty errors
+ if strings.HasPrefix(path, "/dev/loop") {
+ continue
+ }
+
+ mp, err := tryMount(path)
+ if err != nil {
+ continue // Error was reported. Continue looking.
+ }
+ defer doUmount(mp)
+ }
+
+ added1, err := autoImportFromSpool()
+ if err != nil {
+ return err
+ }
+
+ added2, err := autoImportFromAllMounts()
+ if err != nil {
+ return err
+ }
+
+ if added1+added2 > 0 {
+ return autoAddUsers()
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/release"
+)
+
+func makeMockMountInfo(c *C, content string) string {
+ fn := filepath.Join(c.MkDir(), "mountinfo")
+ err := ioutil.WriteFile(fn, []byte(content), 0644)
+ c.Assert(err, IsNil)
+ return fn
+}
+
+func (s *SnapSuite) TestAutoImportAssertsHappy(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ fakeAssertData := []byte("my-assertion")
+
+ n := 0
+ total := 2
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/assertions")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(postData, DeepEquals, fakeAssertData)
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`)
+ n++
+ case 1:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/create-user")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`)
+
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`)
+ n++
+ default:
+ c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n)
+ }
+
+ })
+
+ fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert")
+ err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644)
+ c.Assert(err, IsNil)
+
+ mockMountInfoFmt := `
+24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered`
+ content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn))
+ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content))
+ defer restore()
+
+ l, err := logger.NewConsoleLog(s.stderr, 0)
+ c.Assert(err, IsNil)
+ logger.SetLogger(l)
+
+ rest, err := snap.Parser().ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, `created user "foo"`+"\n")
+ // matches because we may get a:
+ // "WARNING: cannot create syslog logger\n"
+ // in the output
+ c.Check(s.Stderr(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn))
+ c.Check(n, Equals, total)
+}
+
+func (s *SnapSuite) TestAutoImportAssertsNotImportedFromLoop(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ fakeAssertData := []byte("bad-assertion")
+
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ // assertion is ignored, nothing is posted to this endpoint
+ panic("not reached")
+ })
+
+ fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert")
+ err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644)
+ c.Assert(err, IsNil)
+
+ mockMountInfoFmtWithLoop := `
+24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/loop1 rw,errors=remount-ro,data=ordered`
+ content := fmt.Sprintf(mockMountInfoFmtWithLoop, filepath.Dir(fakeAssertsFn))
+ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content))
+ defer restore()
+
+ rest, err := snap.Parser().ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestAutoImportCandidatesHappy(c *C) {
+ dirs := make([]string, 4)
+ args := make([]interface{}, len(dirs))
+ files := make([]string, len(dirs))
+ for i := range dirs {
+ dirs[i] = c.MkDir()
+ args[i] = dirs[i]
+ files[i] = filepath.Join(dirs[i], "auto-import.assert")
+ err := ioutil.WriteFile(files[i], nil, 0644)
+ c.Assert(err, IsNil)
+ }
+
+ mockMountInfoFmtWithLoop := `
+too short
+24 0 8:18 / %[1]s rw,relatime foo ext3 /dev/meep2 no,separator
+24 0 8:18 / %[2]s rw,relatime - ext3 /dev/meep2 rw,errors=remount-ro,data=ordered
+24 0 8:18 / %[3]s rw,relatime opt:1 - ext4 /dev/meep3 rw,errors=remount-ro,data=ordered
+24 0 8:18 / %[4]s rw,relatime opt:1 opt:2 - ext2 /dev/meep1 rw,errors=remount-ro,data=ordered
+`
+
+ content := fmt.Sprintf(mockMountInfoFmtWithLoop, args...)
+ restore := snap.MockMountInfoPath(makeMockMountInfo(c, content))
+ defer restore()
+
+ l, err := snap.AutoImportCandidates()
+ c.Check(err, IsNil)
+ c.Check(l, DeepEquals, files[1:len(files)])
+}
+
+func (s *SnapSuite) TestAutoImportAssertsHappyNotOnClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ fakeAssertData := []byte("my-assertion")
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Errorf("auto-import on classic is disabled, but something tried to do a %q with %s", r.Method, r.URL.Path)
+ })
+
+ fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert")
+ err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644)
+ c.Assert(err, IsNil)
+
+ mockMountInfoFmt := `
+24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered`
+ content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn))
+ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content))
+ defer restore()
+
+ rest, err := snap.Parser().ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "auto-import is disabled on classic\n")
+}
+
+func (s *SnapSuite) TestAutoImportIntoSpool(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+
+ l, err := logger.NewConsoleLog(s.stderr, 0)
+ c.Assert(err, IsNil)
+ logger.SetLogger(l)
+
+ fakeAssertData := []byte("good-assertion")
+
+ // ensure we can not connect
+ snap.ClientConfig.BaseURL = "can-not-connect-to-this-url"
+
+ fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert")
+ err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644)
+ c.Assert(err, IsNil)
+
+ mockMountInfoFmt := `
+24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered`
+ content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn))
+ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content))
+ defer restore()
+
+ rest, err := snap.Parser().ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "")
+ // matches because we may get a:
+ // "WARNING: cannot create syslog logger\n"
+ // in the output
+ c.Check(s.Stderr(), Matches, "(?ms).*queuing for later.*\n")
+
+ files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir)
+ c.Assert(err, IsNil)
+ c.Check(files, HasLen, 1)
+ c.Check(files[0].Name(), Equals, "iOkaeet50rajLvL-0Qsf2ELrTdn3XIXRIBlDewcK02zwRi3_TJlUOTl9AaiDXmDn.assert")
+}
+
+func (s *SnapSuite) TestAutoImportFromSpoolHappy(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ fakeAssertData := []byte("my-assertion")
+
+ n := 0
+ total := 2
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/assertions")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(postData, DeepEquals, fakeAssertData)
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`)
+ n++
+ case 1:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/create-user")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`)
+
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`)
+ n++
+ default:
+ c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n)
+ }
+
+ })
+
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+
+ fakeAssertsFn := filepath.Join(dirs.SnapAssertsSpoolDir, "1234343")
+ err := os.MkdirAll(filepath.Dir(fakeAssertsFn), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644)
+ c.Assert(err, IsNil)
+
+ l, err := logger.NewConsoleLog(s.stderr, 0)
+ c.Assert(err, IsNil)
+ logger.SetLogger(l)
+
+ rest, err := snap.Parser().ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, `created user "foo"`+"\n")
+ // matches because we may get a:
+ // "WARNING: cannot create syslog logger\n"
+ // in the output
+ c.Check(s.Stderr(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn))
+ c.Check(n, Equals, total)
+
+ c.Check(osutil.FileExists(fakeAssertsFn), Equals, false)
+}
+
+func (s *SnapSuite) TestAutoImportIntoSpoolUnhappyTooBig(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+
+ l, err := logger.NewConsoleLog(s.stderr, 0)
+ c.Assert(err, IsNil)
+ logger.SetLogger(l)
+
+ // fake data is bigger than the default assertion limit
+ fakeAssertData := make([]byte, 641*1024)
+
+ // ensure we can not connect
+ snap.ClientConfig.BaseURL = "can-not-connect-to-this-url"
+
+ fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert")
+ err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644)
+ c.Assert(err, IsNil)
+
+ mockMountInfoFmt := `
+24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered`
+ content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn))
+ restore = snap.MockMountInfoPath(makeMockMountInfo(c, content))
+ defer restore()
+
+ _, err = snap.Parser().ParseArgs([]string{"auto-import"})
+ c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdBooted struct{}
+
+func init() {
+ cmd := addCommand("booted",
+ "internal",
+ "internal",
+ func() flags.Commander {
+ return &cmdBooted{}
+ }, nil, nil)
+ cmd.hidden = true
+}
+
+// WARNING: do not remove this command, older systems may still have
+// a systemd snapd.firstboot.service job in /etc/systemd/system
+// that we did not cleanup. so we need this dummy command or
+// those units will start failing.
+func (x *cmdBooted) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ fmt.Fprintf(Stderr, "booted command is deprecated")
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/store"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortBuyHelp = i18n.G("Buys a snap")
+var longBuyHelp = i18n.G(`
+The buy command buys a snap from the store.
+`)
+
+type cmdBuy struct {
+ Positional struct {
+ SnapName remoteSnapName
+ } `positional-args:"yes" required:"yes"`
+}
+
+func init() {
+ addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander {
+ return &cmdBuy{}
+ }, map[string]string{}, []argDesc{{
+ name: "<snap>",
+ desc: i18n.G("Snap name"),
+ }})
+}
+
+func (x *cmdBuy) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ return buySnap(string(x.Positional.SnapName))
+}
+
+func buySnap(snapName string) error {
+ cli := Client()
+
+ user := cli.LoggedInUser()
+ if user == nil {
+ return fmt.Errorf(i18n.G("You need to be logged in to purchase software. Please run 'snap login' and try again."))
+ }
+
+ if strings.ContainsAny(snapName, ":*") {
+ return fmt.Errorf(i18n.G("cannot buy snap: invalid characters in name"))
+ }
+
+ snap, resultInfo, err := cli.FindOne(snapName)
+ if err != nil {
+ return err
+ }
+
+ opts := &store.BuyOptions{
+ SnapID: snap.ID,
+ Currency: resultInfo.SuggestedCurrency,
+ }
+
+ opts.Price, opts.Currency, err = getPrice(snap.Prices, opts.Currency)
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot buy snap: %v"), err)
+ }
+
+ if snap.Status == "available" {
+ return fmt.Errorf(i18n.G("cannot buy snap: it has already been bought"))
+ }
+
+ err = cli.ReadyToBuy()
+ if err != nil {
+ if e, ok := err.(*client.Error); ok {
+ switch e.Kind {
+ case client.ErrorKindNoPaymentMethods:
+ return fmt.Errorf(i18n.G(`You do not have a payment method associated with your account, visit https://my.ubuntu.com/payment/edit to add one.
+Once completed, return here and run 'snap buy %s' again.`), snap.Name)
+ case client.ErrorKindTermsNotAccepted:
+ return fmt.Errorf(i18n.G(`Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms and conditions.
+Once completed, return here and run 'snap buy %s' again.`), snap.Name)
+ }
+ }
+ return err
+ }
+
+ // TRANSLATORS: %q, %q and %s are the snap name, developer, and price. Please wrap the translation at 80 characters.
+ fmt.Fprintf(Stdout, i18n.G(`Please re-enter your Ubuntu One password to purchase %q from %q
+for %s. Press ctrl-c to cancel.`), snap.Name, snap.Developer, formatPrice(opts.Price, opts.Currency))
+ fmt.Fprint(Stdout, "\n")
+
+ err = requestLogin(user.Email)
+ if err != nil {
+ return err
+ }
+
+ _, err = cli.Buy(opts)
+ if err != nil {
+ if e, ok := err.(*client.Error); ok {
+ switch e.Kind {
+ case client.ErrorKindPaymentDeclined:
+ return fmt.Errorf(i18n.G(`Sorry, your payment method has been declined by the issuer. Please review your
+payment details at https://my.ubuntu.com/payment/edit and try again.`))
+ }
+ }
+ return err
+ }
+
+ // TRANSLATORS: %q and %s are the same snap name. Please wrap the translation at 80 characters.
+ fmt.Fprintf(Stdout, i18n.G(`Thanks for purchasing %q. You may now install it on any of your devices
+with 'snap install %s'.`), snapName, snapName)
+ fmt.Fprint(Stdout, "\n")
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+type BuySnapSuite struct {
+ BaseSnapSuite
+}
+
+var _ = check.Suite(&BuySnapSuite{})
+
+type expectedURL struct {
+ Body string
+ Checker func(r *http.Request)
+
+ callCount int
+}
+
+type expectedMethod map[string]*expectedURL
+
+type expectedMethods map[string]*expectedMethod
+
+type buyTestMockSnapServer struct {
+ ExpectedMethods expectedMethods
+
+ Checker *check.C
+}
+
+func (s *buyTestMockSnapServer) serveHttp(w http.ResponseWriter, r *http.Request) {
+ method := s.ExpectedMethods[r.Method]
+ if method == nil || len(*method) == 0 {
+ s.Checker.Fatalf("unexpected HTTP method %s", r.Method)
+ }
+
+ url := (*method)[r.URL.Path]
+ if url == nil {
+ s.Checker.Fatalf("unexpected URL %q", r.URL.Path)
+ }
+
+ if url.Checker != nil {
+ url.Checker(r)
+ }
+ fmt.Fprintln(w, url.Body)
+ url.callCount++
+}
+
+func (s *buyTestMockSnapServer) checkCounts() {
+ for _, method := range s.ExpectedMethods {
+ for _, url := range *method {
+ s.Checker.Check(url.callCount, check.Equals, 1)
+ }
+ }
+}
+
+func (s *BuySnapSuite) SetUpTest(c *check.C) {
+ s.BaseSnapSuite.SetUpTest(c)
+ s.Login(c)
+}
+
+func (s *BuySnapSuite) TearDownTest(c *check.C) {
+ s.Logout(c)
+ s.BaseSnapSuite.TearDownTest(c)
+}
+
+func (s *BuySnapSuite) TestBuyHelp(c *check.C) {
+ _, err := snap.Parser().ParseArgs([]string{"buy"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, "the required argument `<snap>` was not provided")
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) {
+ _, err := snap.Parser().ParseArgs([]string{"buy", "a:b"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name")
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+
+ _, err = snap.Parser().ParseArgs([]string{"buy", "c*d"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name")
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const buyFreeSnapFailsFindJson = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": [
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "download-size": 65536,
+ "icon": "",
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "name": "hello",
+ "private": false,
+ "resource": "/v2/snaps/hello",
+ "revision": "1",
+ "status": "available",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "type": "app",
+ "version": "2.10"
+ }
+ ],
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func (s *BuySnapSuite) TestBuyFreeSnapFails(c *check.C) {
+ mockServer := &buyTestMockSnapServer{
+ ExpectedMethods: expectedMethods{
+ "GET": &expectedMethod{
+ "/v2/find": &expectedURL{
+ Body: buyFreeSnapFailsFindJson,
+ },
+ },
+ },
+ Checker: c,
+ }
+ defer mockServer.checkCounts()
+ s.RedirectClientToTestServer(mockServer.serveHttp)
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, "cannot buy snap: snap is free")
+ c.Assert(rest, check.DeepEquals, []string{"hello"})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const buySnapFindJson = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": [
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "download-size": 65536,
+ "icon": "",
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "name": "hello",
+ "private": false,
+ "resource": "/v2/snaps/hello",
+ "revision": "1",
+ "status": "priced",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "type": "app",
+ "version": "2.10",
+ "prices": {"USD": 3.99, "GBP": 2.99}
+ }
+ ],
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func buySnapFindURL(c *check.C) *expectedURL {
+ return &expectedURL{
+ Body: buySnapFindJson,
+ Checker: func(r *http.Request) {
+ c.Check(r.URL.Query().Get("name"), check.Equals, "hello")
+ },
+ }
+}
+
+const buyReadyJson = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": true,
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func buyReady(c *check.C) *expectedURL {
+ return &expectedURL{
+ Body: buyReadyJson,
+ }
+}
+
+const buySnapJson = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": {
+ "state": "Complete"
+ },
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+const loginJson = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": {
+ "id": 1,
+ "username": "username",
+ "email": "hello@mail.com",
+ "macaroon": "1234abcd",
+ "discharges": ["a", "b", "c"]
+ },
+ "sources": [
+ "store"
+ ]
+}
+`
+
+func (s *BuySnapSuite) TestBuySnapSuccess(c *check.C) {
+ mockServer := &buyTestMockSnapServer{
+ ExpectedMethods: expectedMethods{
+ "GET": &expectedMethod{
+ "/v2/find": buySnapFindURL(c),
+ "/v2/buy/ready": buyReady(c),
+ },
+ "POST": &expectedMethod{
+ "/v2/login": &expectedURL{
+ Body: loginJson,
+ },
+ "/v2/buy": &expectedURL{
+ Body: buySnapJson,
+ Checker: func(r *http.Request) {
+ var postData struct {
+ SnapID string `json:"snap-id"`
+ Price float64 `json:"price"`
+ Currency string `json:"currency"`
+ }
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&postData)
+ c.Assert(err, check.IsNil)
+
+ c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6")
+ c.Check(postData.Price, check.Equals, 2.99)
+ c.Check(postData.Currency, check.Equals, "GBP")
+ },
+ },
+ },
+ },
+ Checker: c,
+ }
+ defer mockServer.checkCounts()
+ s.RedirectClientToTestServer(mockServer.serveHttp)
+
+ // Confirm the purchase.
+ s.password = "the password"
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Check(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical"
+for 2.99GBP. Press ctrl-c to cancel.
+Password of "hello@mail.com":
+Thanks for purchasing "hello". You may now install it on any of your devices
+with 'snap install hello'.
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const buySnapPaymentDeclinedJson = `
+{
+ "type": "error",
+ "result": {
+ "message": "payment declined",
+ "kind": "payment-declined"
+ },
+ "status-code": 400
+}
+`
+
+func (s *BuySnapSuite) TestBuySnapPaymentDeclined(c *check.C) {
+ mockServer := &buyTestMockSnapServer{
+ ExpectedMethods: expectedMethods{
+ "GET": &expectedMethod{
+ "/v2/find": buySnapFindURL(c),
+ "/v2/buy/ready": buyReady(c),
+ },
+ "POST": &expectedMethod{
+ "/v2/login": &expectedURL{
+ Body: loginJson,
+ },
+ "/v2/buy": &expectedURL{
+ Body: buySnapPaymentDeclinedJson,
+ Checker: func(r *http.Request) {
+ var postData struct {
+ SnapID string `json:"snap-id"`
+ Price float64 `json:"price"`
+ Currency string `json:"currency"`
+ }
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&postData)
+ c.Assert(err, check.IsNil)
+
+ c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6")
+ c.Check(postData.Price, check.Equals, 2.99)
+ c.Check(postData.Currency, check.Equals, "GBP")
+ },
+ },
+ },
+ },
+ Checker: c,
+ }
+ defer mockServer.checkCounts()
+ s.RedirectClientToTestServer(mockServer.serveHttp)
+
+ // Confirm the purchase.
+ s.password = "the password"
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, `Sorry, your payment method has been declined by the issuer. Please review your
+payment details at https://my.ubuntu.com/payment/edit and try again.`)
+ c.Check(rest, check.DeepEquals, []string{"hello"})
+ c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical"
+for 2.99GBP. Press ctrl-c to cancel.
+Password of "hello@mail.com":
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const readyToBuyNoPaymentMethodJson = `
+{
+ "type": "error",
+ "result": {
+ "message": "no payment methods",
+ "kind": "no-payment-methods"
+ },
+ "status-code": 400
+}`
+
+func (s *BuySnapSuite) TestBuySnapFailsNoPaymentMethod(c *check.C) {
+ mockServer := &buyTestMockSnapServer{
+ ExpectedMethods: expectedMethods{
+ "GET": &expectedMethod{
+ "/v2/find": buySnapFindURL(c),
+ "/v2/buy/ready": &expectedURL{
+ Body: readyToBuyNoPaymentMethodJson,
+ },
+ },
+ },
+ Checker: c,
+ }
+ defer mockServer.checkCounts()
+ s.RedirectClientToTestServer(mockServer.serveHttp)
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, `You do not have a payment method associated with your account, visit https://my.ubuntu.com/payment/edit to add one.
+Once completed, return here and run 'snap buy hello' again.`)
+ c.Check(rest, check.DeepEquals, []string{"hello"})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const readyToBuyNotAcceptedTermsJson = `
+{
+ "type": "error",
+ "result": {
+ "message": "terms of service not accepted",
+ "kind": "terms-not-accepted"
+ },
+ "status-code": 400
+}`
+
+func (s *BuySnapSuite) TestBuySnapFailsNotAcceptedTerms(c *check.C) {
+ mockServer := &buyTestMockSnapServer{
+ ExpectedMethods: expectedMethods{
+ "GET": &expectedMethod{
+ "/v2/find": buySnapFindURL(c),
+ "/v2/buy/ready": &expectedURL{
+ Body: readyToBuyNotAcceptedTermsJson,
+ },
+ },
+ },
+ Checker: c,
+ }
+ defer mockServer.checkCounts()
+ s.RedirectClientToTestServer(mockServer.serveHttp)
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, `Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms and conditions.
+Once completed, return here and run 'snap buy hello' again.`)
+ c.Check(rest, check.DeepEquals, []string{"hello"})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *BuySnapSuite) TestBuyFailsWithoutLogin(c *check.C) {
+ // We don't login here
+ s.Logout(c)
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Check(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.")
+ c.Check(rest, check.DeepEquals, []string{"hello"})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "regexp"
+ "sort"
+ "time"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortChangesHelp = i18n.G("List system changes")
+var shortChangeHelp = i18n.G("List a change's tasks")
+var longChangesHelp = i18n.G(`
+The changes command displays a summary of the recent system changes performed.`)
+var longChangeHelp = i18n.G(`
+The change command displays a summary of tasks associated to an individual change.`)
+
+type cmdChanges struct {
+ Positional struct {
+ Snap string `positional-arg-name:"<snap>"`
+ } `positional-args:"yes"`
+}
+
+type cmdChange struct {
+ Positional struct {
+ ID changeID `positional-arg-name:"<id>" required:"yes"`
+ } `positional-args:"yes"`
+}
+
+func init() {
+ addCommand("changes", shortChangesHelp, longChangesHelp, func() flags.Commander { return &cmdChanges{} }, nil, nil)
+ addCommand("change", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdChange{} }, nil, nil)
+}
+
+type changesByTime []*client.Change
+
+func (s changesByTime) Len() int { return len(s) }
+func (s changesByTime) Less(i, j int) bool { return s[i].SpawnTime.Before(s[j].SpawnTime) }
+func (s changesByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+var allDigits = regexp.MustCompile(`^[0-9]+$`).MatchString
+
+func (c *cmdChanges) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ if allDigits(c.Positional.Snap) {
+ // TRANSLATORS: the %s is the argument given by the user to "snap changes"
+ return fmt.Errorf(i18n.G(`"snap changes" command expects a snap name, try: "snap change %s"`), c.Positional.Snap)
+ }
+
+ if c.Positional.Snap == "everything" {
+ fmt.Fprintln(Stdout, i18n.G("Yes, yes it does."))
+ return nil
+ }
+
+ opts := client.ChangesOptions{
+ SnapName: c.Positional.Snap,
+ Selector: client.ChangesAll,
+ }
+
+ cli := Client()
+ changes, err := cli.Changes(&opts)
+ if err != nil {
+ return err
+ }
+
+ if len(changes) == 0 {
+ return fmt.Errorf(i18n.G("no changes found"))
+ }
+
+ sort.Sort(changesByTime(changes))
+
+ w := tabWriter()
+
+ fmt.Fprintf(w, i18n.G("ID\tStatus\tSpawn\tReady\tSummary\n"))
+ for _, chg := range changes {
+ spawnTime := chg.SpawnTime.UTC().Format(time.RFC3339)
+ readyTime := chg.ReadyTime.UTC().Format(time.RFC3339)
+ if chg.ReadyTime.IsZero() {
+ readyTime = "-"
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", chg.ID, chg.Status, spawnTime, readyTime, chg.Summary)
+ }
+
+ w.Flush()
+ fmt.Fprintln(Stdout)
+
+ return nil
+}
+
+func (c *cmdChange) Execute([]string) error {
+ cli := Client()
+ chg, err := cli.Change(string(c.Positional.ID))
+ if err != nil {
+ return err
+ }
+
+ w := tabWriter()
+
+ fmt.Fprintf(w, i18n.G("Status\tSpawn\tReady\tSummary\n"))
+ for _, t := range chg.Tasks {
+ spawnTime := t.SpawnTime.UTC().Format(time.RFC3339)
+ readyTime := t.ReadyTime.UTC().Format(time.RFC3339)
+ if t.ReadyTime.IsZero() {
+ readyTime = "-"
+ }
+ summary := t.Summary
+ if t.Status == "Doing" && t.Progress.Total > 1 {
+ summary = fmt.Sprintf("%s (%.2f%%)", summary, float64(t.Progress.Done)/float64(t.Progress.Total)*100.0)
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Status, spawnTime, readyTime, summary)
+ }
+
+ w.Flush()
+
+ for _, t := range chg.Tasks {
+ if len(t.Log) == 0 {
+ continue
+ }
+ fmt.Fprintln(Stdout)
+ fmt.Fprintln(Stdout, line)
+ fmt.Fprintln(Stdout, t.Summary)
+ fmt.Fprintln(Stdout)
+ for _, line := range t.Log {
+ fmt.Fprintln(Stdout, line)
+ }
+ }
+
+ fmt.Fprintln(Stdout)
+
+ return nil
+}
+
+const line = "......................................................................"
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+var mockChangeJSON = `{"type": "sync", "result": {
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Do",
+ "ready": false,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:04Z",
+ "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}]
+}}`
+
+func (s *SnapSuite) TestChangeSimple(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, mockChangeJSON)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"change", "42"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary
+Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+var mockChangeInProgressJSON = `{"type": "sync", "result": {
+ "id": "uno",
+ "kind": "foo",
+ "summary": "...",
+ "status": "Do",
+ "ready": false,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:04Z",
+ "tasks": [{"kind": "bar", "summary": "some summary", "status": "Doing", "progress": {"done": 50, "total": 100}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}]
+}}`
+
+func (s *SnapSuite) TestChangeProgress(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, mockChangeInProgressJSON)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"change", "42"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary
+Doing +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary \(50.00%\)
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdConnect struct {
+ Positionals struct {
+ PlugSpec SnapAndName `required:"yes"`
+ SlotSpec SnapAndName
+ } `positional-args:"true"`
+}
+
+var shortConnectHelp = i18n.G("Connects a plug to a slot")
+var longConnectHelp = i18n.G(`
+The connect command connects a plug to a slot.
+It may be called in the following ways:
+
+$ snap connect <snap>:<plug> <snap>:<slot>
+
+Connects the provided plug to the given slot.
+
+$ snap connect <snap>:<plug> <snap>
+
+Connects the specific plug to the only slot in the provided snap that matches
+the connected interface. If more than one potential slot exists, the command
+fails.
+
+$ snap connect <snap>:<plug>
+
+Connects the provided plug to the slot in the core snap with a name matching
+the plug name.
+`)
+
+func init() {
+ addCommand("connect", shortConnectHelp, longConnectHelp, func() flags.Commander {
+ return &cmdConnect{}
+ }, nil, []argDesc{
+ {name: i18n.G("<snap>:<plug>")},
+ {name: i18n.G("<snap>:<slot>")},
+ })
+}
+
+func (x *cmdConnect) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ // snap connect <plug> <snap>[:<slot>]
+ if x.Positionals.PlugSpec.Snap != "" && x.Positionals.PlugSpec.Name == "" {
+ // Move the value of .Snap to .Name and keep .Snap empty
+ x.Positionals.PlugSpec.Name = x.Positionals.PlugSpec.Snap
+ x.Positionals.PlugSpec.Snap = ""
+ }
+
+ cli := Client()
+ id, err := cli.Connect(x.Positionals.PlugSpec.Snap, x.Positionals.PlugSpec.Name, x.Positionals.SlotSpec.Snap, x.Positionals.SlotSpec.Name)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, id)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestConnectHelp(c *C) {
+ msg := `Usage:
+ snap.test [OPTIONS] connect [<snap>:<plug>] [<snap>:<slot>]
+
+The connect command connects a plug to a slot.
+It may be called in the following ways:
+
+$ snap connect <snap>:<plug> <snap>:<slot>
+
+Connects the provided plug to the given slot.
+
+$ snap connect <snap>:<plug> <snap>
+
+Connects the specific plug to the only slot in the provided snap that matches
+the connected interface. If more than one potential slot exists, the command
+fails.
+
+$ snap connect <snap>:<plug>
+
+Connects the provided plug to the slot in the core snap with a name matching
+the plug name.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+`
+ rest, err := Parser().ParseArgs([]string{"connect", "--help"})
+ c.Assert(err.Error(), Equals, msg)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestConnectExplicitEverything(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "connect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer:slot"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestConnectExplicitPlugImplicitSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "connect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestConnectImplicitPlugExplicitSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "connect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer:slot"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestConnectImplicitPlugImplicitSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "connect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/jessevdk/go-flags"
+ "golang.org/x/crypto/ssh/terminal"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdCreateKey struct {
+ Positional struct {
+ KeyName string
+ } `positional-args:"true"`
+}
+
+func init() {
+ cmd := addCommand("create-key",
+ i18n.G("Create cryptographic key pair"),
+ i18n.G("Create a cryptographic key pair that can be used for signing assertions."),
+ func() flags.Commander {
+ return &cmdCreateKey{}
+ }, nil, []argDesc{{
+ name: i18n.G("<key-name>"),
+ desc: i18n.G("Name of key to create; defaults to 'default'"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdCreateKey) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ keyName := x.Positional.KeyName
+ if keyName == "" {
+ keyName = "default"
+ }
+ if !asserts.IsValidAccountKeyName(keyName) {
+ return fmt.Errorf(i18n.G("key name %q is not valid; only ASCII letters, digits, and hyphens are allowed"), keyName)
+ }
+
+ fmt.Fprint(Stdout, i18n.G("Passphrase: "))
+ passphrase, err := terminal.ReadPassword(0)
+ fmt.Fprint(Stdout, "\n")
+ if err != nil {
+ return err
+ }
+ fmt.Fprint(Stdout, i18n.G("Confirm passphrase: "))
+ confirmPassphrase, err := terminal.ReadPassword(0)
+ fmt.Fprint(Stdout, "\n")
+ if err != nil {
+ return err
+ }
+ if string(passphrase) != string(confirmPassphrase) {
+ return errors.New("passphrases do not match")
+ }
+ if err != nil {
+ return err
+ }
+
+ manager := asserts.NewGPGKeypairManager()
+ return manager.Generate(string(passphrase), keyName)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestCreateKeyInvalidCharacters(c *C) {
+ _, err := snap.Parser().ParseArgs([]string{"create-key", "a b"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "key name \"a b\" is not valid; only ASCII letters, digits, and hyphens are allowed")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortCreateUserHelp = i18n.G("Creates a local system user")
+var longCreateUserHelp = i18n.G(`
+The create-user command creates a local system user with the username and SSH
+keys registered on the store account identified by the provided email address.
+
+An account can be setup at https://login.ubuntu.com.
+`)
+
+type cmdCreateUser struct {
+ Positional struct {
+ Email string
+ } `positional-args:"yes"`
+
+ JSON bool `long:"json"`
+ Sudoer bool `long:"sudoer"`
+ Known bool `long:"known"`
+ ForceManaged bool `long:"force-managed"`
+}
+
+func init() {
+ cmd := addCommand("create-user", shortCreateUserHelp, longCreateUserHelp, func() flags.Commander { return &cmdCreateUser{} },
+ map[string]string{
+ "json": i18n.G("Output results in JSON format"),
+ "sudoer": i18n.G("Grant sudo access to the created user"),
+ "known": i18n.G("Use known assertions for user creation"),
+ "force-managed": i18n.G("Force adding the user, even if the device is already managed"),
+ }, []argDesc{{
+ // TRANSLATORS: noun
+ name: i18n.G("<email>"),
+ // TRANSLATORS: note users on login.ubuntu.com can have multiple email addresses
+ desc: i18n.G("An email of a user on login.ubuntu.com"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdCreateUser) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ cli := Client()
+
+ options := client.CreateUserOptions{
+ Email: x.Positional.Email,
+ Sudoer: x.Sudoer,
+ Known: x.Known,
+ ForceManaged: x.ForceManaged,
+ }
+
+ var results []*client.CreateUserResult
+ var result *client.CreateUserResult
+ var err error
+
+ if options.Email == "" && options.Known {
+ results, err = cli.CreateUsers([]*client.CreateUserOptions{&options})
+ } else {
+ result, err = cli.CreateUser(&options)
+ if err == nil {
+ results = append(results, result)
+ }
+ }
+
+ createErr := err
+
+ // Print results regardless of error because some users may have been created.
+ if x.JSON {
+ var data []byte
+ if result != nil {
+ data, err = json.Marshal(result)
+ } else if len(results) > 0 {
+ data, err = json.Marshal(results)
+ }
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(Stdout, "%s\n", data)
+ } else {
+ for _, result := range results {
+ fmt.Fprintf(Stdout, i18n.G("created user %q\n"), result.Username)
+ }
+ }
+
+ return createErr
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func makeCreateUserChecker(c *check.C, n *int, email string, sudoer, known bool) func(w http.ResponseWriter, r *http.Request) {
+ f := func(w http.ResponseWriter, r *http.Request) {
+ switch *n {
+ case 0:
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/create-user")
+ var gotBody map[string]interface{}
+ dec := json.NewDecoder(r.Body)
+ err := dec.Decode(&gotBody)
+ c.Assert(err, check.IsNil)
+
+ wantBody := make(map[string]interface{})
+ if email != "" {
+ wantBody["email"] = "one@email.com"
+ }
+ if sudoer {
+ wantBody["sudoer"] = true
+ }
+ if known {
+ wantBody["known"] = true
+ }
+ c.Check(gotBody, check.DeepEquals, wantBody)
+
+ if email == "" {
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`)
+ } else {
+ fmt.Fprintln(w, `{"type": "sync", "result": {"username": "karl", "ssh-keys": ["a","b"]}}`)
+ }
+ default:
+ c.Fatalf("got too many requests (now on %d)", *n+1)
+ }
+
+ *n++
+ }
+ return f
+}
+
+func (s *SnapSuite) TestCreateUserNoSudoer(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false))
+
+ rest, err := snap.Parser().ParseArgs([]string{"create-user", "one@email.com"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+ c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n")
+ c.Assert(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestCreateUserSudoer(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", true, false))
+
+ rest, err := snap.Parser().ParseArgs([]string{"create-user", "--sudoer", "one@email.com"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+ c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n")
+ c.Assert(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestCreateUserJSON(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false))
+
+ expectedResponse := &client.CreateUserResult{
+ Username: "karl",
+ SSHKeys: []string{"a", "b"},
+ }
+ actualResponse := &client.CreateUserResult{}
+
+ rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "one@email.com"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+ json.Unmarshal(s.stdout.Bytes(), actualResponse)
+ c.Assert(actualResponse, check.DeepEquals, expectedResponse)
+ c.Assert(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestCreateUserNoEmailJSON(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true))
+
+ var expectedResponse = []*client.CreateUserResult{{
+ Username: "karl",
+ SSHKeys: []string{"a", "b"},
+ }}
+ var actualResponse []*client.CreateUserResult
+
+ rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "--known"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+ json.Unmarshal(s.stdout.Bytes(), &actualResponse)
+ c.Assert(actualResponse, check.DeepEquals, expectedResponse)
+ c.Assert(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestCreateUserKnown(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, true))
+
+ rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known", "one@email.com"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestCreateUserKnownNoEmail(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true))
+
+ rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdDeleteKey struct {
+ Positional struct {
+ KeyName keyName
+ } `positional-args:"true" required:"true"`
+}
+
+func init() {
+ cmd := addCommand("delete-key",
+ i18n.G("Delete cryptographic key pair"),
+ i18n.G("Delete the local cryptographic key pair with the given name."),
+ func() flags.Commander {
+ return &cmdDeleteKey{}
+ }, nil, []argDesc{{
+ name: i18n.G("<key-name>"),
+ desc: i18n.G("Name of key to delete"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdDeleteKey) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ manager := asserts.NewGPGKeypairManager()
+ return manager.Delete(string(x.Positional.KeyName))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "encoding/json"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapKeysSuite) TestDeleteKeyRequiresName(c *C) {
+ _, err := snap.Parser().ParseArgs([]string{"delete-key"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "the required argument `<key-name>` was not provided")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestDeleteKeyNonexistent(c *C) {
+ _, err := snap.Parser().ParseArgs([]string{"delete-key", "nonexistent"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestDeleteKey(c *C) {
+ rest, err := snap.Parser().ParseArgs([]string{"delete-key", "another"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+ _, err = snap.Parser().ParseArgs([]string{"keys", "--json"})
+ c.Assert(err, IsNil)
+ expectedResponse := []snap.Key{
+ {
+ Name: "default",
+ Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ",
+ },
+ }
+ var obtainedResponse []snap.Key
+ json.Unmarshal(s.stdout.Bytes(), &obtainedResponse)
+ c.Check(obtainedResponse, DeepEquals, expectedResponse)
+ c.Check(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdDisconnect struct {
+ Positionals struct {
+ Offer SnapAndName `required:"true"`
+ Use SnapAndName
+ } `positional-args:"true"`
+}
+
+var shortDisconnectHelp = i18n.G("Disconnects a plug from a slot")
+var longDisconnectHelp = i18n.G(`
+The disconnect command disconnects a plug from a slot.
+It may be called in the following ways:
+
+$ snap disconnect <snap>:<plug> <snap>:<slot>
+
+Disconnects the specific plug from the specific slot.
+
+$ snap disconnect <snap>:<slot or plug>
+
+Disconnects everything from the provided plug or slot.
+The snap name may be omitted for the core snap.
+`)
+
+func init() {
+ addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander {
+ return &cmdDisconnect{}
+ }, nil, []argDesc{
+ {name: i18n.G("<snap>:<plug>")},
+ {name: i18n.G("<snap>:<slot>")},
+ })
+}
+
+func (x *cmdDisconnect) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ // snap disconnect <snap>:<slot>
+ // snap disconnect <snap>
+ if x.Positionals.Use.Snap == "" && x.Positionals.Use.Name == "" {
+ // Swap Offer and Use around
+ x.Positionals.Offer, x.Positionals.Use = x.Positionals.Use, x.Positionals.Offer
+ }
+ if x.Positionals.Use.Name == "" {
+ return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", x.Positionals.Use.Snap)
+ }
+
+ cli := Client()
+ id, err := cli.Disconnect(x.Positionals.Offer.Snap, x.Positionals.Offer.Name, x.Positionals.Use.Snap, x.Positionals.Use.Name)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, id)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestDisconnectHelp(c *C) {
+ msg := `Usage:
+ snap.test [OPTIONS] disconnect [<snap>:<plug>] [<snap>:<slot>]
+
+The disconnect command disconnects a plug from a slot.
+It may be called in the following ways:
+
+$ snap disconnect <snap>:<plug> <snap>:<slot>
+
+Disconnects the specific plug from the specific slot.
+
+$ snap disconnect <snap>:<slot or plug>
+
+Disconnects everything from the provided plug or slot.
+The snap name may be omitted for the core snap.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+`
+ rest, err := Parser().ParseArgs([]string{"disconnect", "--help"})
+ c.Assert(err.Error(), Equals, msg)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestDisconnectExplicitEverything(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "disconnect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"disconnect", "producer:plug", "consumer:slot"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDisconnectEverythingFromSpecificSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "disconnect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "",
+ "plug": "",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:slot"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnapPlugOrSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "disconnect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "",
+ "plug": "",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "plug-or-slot",
+ },
+ },
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:plug-or-slot"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnap(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Fatalf("expected nothing to reach the server")
+ })
+ rest, err := Parser().ParseArgs([]string{"disconnect", "consumer"})
+ c.Assert(err, ErrorMatches, `please provide the plug or slot name to disconnect from snap "consumer"`)
+ c.Assert(rest, DeepEquals, []string{"consumer"})
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/image"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+type cmdDownload struct {
+ channelMixin
+ Revision string `long:"revision"`
+
+ Positional struct {
+ Snap remoteSnapName
+ } `positional-args:"true" required:"true"`
+}
+
+var shortDownloadHelp = i18n.G("Downloads the given snap")
+var longDownloadHelp = i18n.G(`
+The download command downloads the given snap and its supporting assertions
+to the current directory under .snap and .assert file extensions, respectively.
+`)
+
+func init() {
+ addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander {
+ return &cmdDownload{}
+ }, channelDescs.also(map[string]string{
+ "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"),
+ }), []argDesc{{
+ name: "<snap>",
+ desc: i18n.G("Snap name"),
+ }})
+}
+
+func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info, dlOpts *image.DownloadOptions) error {
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: sysdb.Trusted(),
+ })
+ if err != nil {
+ return err
+ }
+
+ assertPath := strings.TrimSuffix(snapPath, filepath.Ext(snapPath)) + ".assert"
+ w, err := os.Create(assertPath)
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot create assertions file: %v"), err)
+ }
+ defer w.Close()
+
+ encoder := asserts.NewEncoder(w)
+ save := func(a asserts.Assertion) error {
+ return encoder.Encode(a)
+ }
+ f := image.StoreAssertionFetcher(sto, dlOpts, db, save)
+
+ return image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db)
+}
+
+func (x *cmdDownload) Execute(args []string) error {
+ if err := x.setChannelFromCommandline(); err != nil {
+ return err
+ }
+
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ var revision snap.Revision
+ if x.Revision == "" {
+ revision = snap.R(0)
+ } else {
+ var err error
+ revision, err = snap.ParseRevision(x.Revision)
+ if err != nil {
+ return err
+ }
+ }
+
+ snapName := string(x.Positional.Snap)
+
+ // FIXME: set auth context
+ var authContext auth.AuthContext
+ var user *auth.UserState
+
+ sto := store.New(nil, authContext)
+ // we always allow devmode for downloads
+ devMode := true
+
+ dlOpts := image.DownloadOptions{
+ TargetDir: "", // cwd
+ DevMode: devMode,
+ Channel: x.Channel,
+ User: user,
+ }
+
+ fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName)
+ snapPath, snapInfo, err := image.DownloadSnap(sto, snapName, revision, &dlOpts)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(Stderr, i18n.G("Fetching assertions for %q\n"), snapName)
+ err = fetchSnapAssertions(sto, snapPath, snapInfo, &dlOpts)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdExperimental struct{}
+
+var shortExperimentalHelp = i18n.G("Runs unsupported experimental commands")
+var longExperimentalHelp = i18n.G(`
+The experimental command contains a selection of additional sub-commands.
+
+Experimental commands can be removed without notice and may not work on
+non-development systems.
+`)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdExportKey struct {
+ Account string `long:"account"`
+ Positional struct {
+ KeyName keyName
+ } `positional-args:"true"`
+}
+
+func init() {
+ cmd := addCommand("export-key",
+ i18n.G("Export cryptographic public key"),
+ i18n.G("Export a public key assertion body that may be imported by other systems."),
+ func() flags.Commander {
+ return &cmdExportKey{}
+ }, map[string]string{
+ "account": i18n.G("Format public key material as a request for an account-key for this account-id"),
+ }, []argDesc{{
+ name: i18n.G("<key-name>"),
+ desc: i18n.G("Name of key to export"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdExportKey) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ keyName := string(x.Positional.KeyName)
+ if keyName == "" {
+ keyName = "default"
+ }
+
+ manager := asserts.NewGPGKeypairManager()
+ if x.Account != "" {
+ privKey, err := manager.GetByName(keyName)
+ if err != nil {
+ return err
+ }
+ pubKey := privKey.PublicKey()
+ headers := map[string]interface{}{
+ "account-id": x.Account,
+ "name": keyName,
+ "public-key-sha3-384": pubKey.ID(),
+ "since": time.Now().UTC().Format(time.RFC3339),
+ // XXX: To support revocation, we need to check for matching known assertions and set a suitable revision if we find one.
+ }
+ body, err := asserts.EncodePublicKey(pubKey)
+ if err != nil {
+ return err
+ }
+ assertion, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, body, privKey)
+ if err != nil {
+ return err
+ }
+ fmt.Fprint(Stdout, string(asserts.Encode(assertion)))
+ } else {
+ encoded, err := manager.Export(keyName)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(Stdout, "%s\n", encoded)
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapKeysSuite) TestExportKeyNonexistent(c *C) {
+ _, err := snap.Parser().ParseArgs([]string{"export-key", "nonexistent"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestExportKeyDefault(c *C) {
+ rest, err := snap.Parser().ParseArgs([]string{"export-key"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(pubKey.ID(), Equals, "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestExportKeyNonDefault(c *C) {
+ rest, err := snap.Parser().ParseArgs([]string{"export-key", "another"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(pubKey.ID(), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestExportKeyAccount(c *C) {
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ storeSigning := assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey)
+ manager := asserts.NewGPGKeypairManager()
+ assertstest.NewAccount(storeSigning, "developer1", nil, "")
+ rest, err := snap.Parser().ParseArgs([]string{"export-key", "another", "--account=developer1"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ assertion, err := asserts.Decode(s.stdout.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(assertion.Type(), Equals, asserts.AccountKeyRequestType)
+ c.Check(assertion.Revision(), Equals, 0)
+ c.Check(assertion.HeaderString("account-id"), Equals, "developer1")
+ c.Check(assertion.HeaderString("name"), Equals, "another")
+ c.Check(assertion.HeaderString("public-key-sha3-384"), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L")
+ since, err := time.Parse(time.RFC3339, assertion.HeaderString("since"))
+ c.Assert(err, IsNil)
+ zone, offset := since.Zone()
+ c.Check(zone, Equals, "UTC")
+ c.Check(offset, Equals, 0)
+ c.Check(s.Stderr(), Equals, "")
+ privKey, err := manager.Get(assertion.HeaderString("public-key-sha3-384"))
+ c.Assert(err, IsNil)
+ err = asserts.SignatureCheck(assertion, privKey.PublicKey())
+ c.Assert(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortFindHelp = i18n.G("Finds packages to install")
+var longFindHelp = i18n.G(`
+The find command queries the store for available packages.
+`)
+
+func getPrice(prices map[string]float64, currency string) (float64, string, error) {
+ // If there are no prices, then the snap is free
+ if len(prices) == 0 {
+ // TRANSLATORS: free as in gratis
+ return 0, "", errors.New(i18n.G("snap is free"))
+ }
+
+ // Look up the price by currency code
+ val, ok := prices[currency]
+
+ // Fall back to dollars
+ if !ok {
+ currency = "USD"
+ val, ok = prices["USD"]
+ }
+
+ // If there aren't even dollars, grab the first currency,
+ // ordered alphabetically by currency code
+ if !ok {
+ currency = "ZZZ"
+ for c, v := range prices {
+ if c < currency {
+ currency, val = c, v
+ }
+ }
+ }
+
+ return val, currency, nil
+}
+
+type SectionName string
+
+func (s SectionName) Complete(match string) []flags.Completion {
+ cli := Client()
+ sections, err := cli.Sections()
+ if err != nil {
+ return nil
+ }
+ ret := make([]flags.Completion, 0, len(sections))
+ for _, s := range sections {
+ if strings.HasPrefix(s, match) {
+ ret = append(ret, flags.Completion{Item: s})
+ }
+ }
+ return ret
+}
+
+type cmdFind struct {
+ Private bool `long:"private"`
+ Section SectionName `long:"section"`
+ Positional struct {
+ Query string
+ } `positional-args:"yes"`
+}
+
+func init() {
+ addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander {
+ return &cmdFind{}
+ }, map[string]string{
+ "private": i18n.G("Search private snaps"),
+ "section": i18n.G("Restrict the search to a given section"),
+ }, []argDesc{{name: i18n.G("<query>")}})
+}
+
+func (x *cmdFind) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ // magic! `snap find` returns the featured snaps
+ if x.Positional.Query == "" && x.Section == "" {
+ x.Section = "featured"
+ }
+
+ return findSnaps(&client.FindOptions{
+ Private: x.Private,
+ Section: string(x.Section),
+ Query: x.Positional.Query,
+ })
+}
+
+func findSnaps(opts *client.FindOptions) error {
+ cli := Client()
+ snaps, resInfo, err := cli.Find(opts)
+ if err != nil {
+ return err
+ }
+
+ if len(snaps) == 0 {
+ // TRANSLATORS: the %q is the (quoted) query the user entered
+ fmt.Fprintf(Stderr, i18n.G("The search %q returned 0 snaps\n"), opts.Query)
+ return nil
+ }
+
+ w := tabWriter()
+ defer w.Flush()
+
+ fmt.Fprintln(w, i18n.G("Name\tVersion\tDeveloper\tNotes\tSummary"))
+
+ for _, snap := range snaps {
+ // TODO: get snap.Publisher, so we can only show snap.Developer if it's different
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Developer, NotesFromRemote(snap, resInfo), snap.Summary)
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/jessevdk/go-flags"
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+const findJSON = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": [
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "download-size": 65536,
+ "icon": "",
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "name": "hello",
+ "private": false,
+ "resource": "/v2/snaps/hello",
+ "revision": "1",
+ "status": "available",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "type": "app",
+ "version": "2.10"
+ },
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "This is a simple hello world example.",
+ "developer": "canonical",
+ "download-size": 20480,
+ "icon": "",
+ "id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "name": "hello-world",
+ "private": false,
+ "resource": "/v2/snaps/hello-world",
+ "revision": "26",
+ "status": "available",
+ "summary": "Hello world example",
+ "type": "app",
+ "version": "6.1"
+ },
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "1.0GB",
+ "developer": "noise",
+ "download-size": 512004096,
+ "icon": "",
+ "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne",
+ "name": "hello-huge",
+ "private": false,
+ "resource": "/v2/snaps/hello-huge",
+ "revision": "1",
+ "status": "available",
+ "summary": "a really big snap",
+ "type": "app",
+ "version": "1.0"
+ }
+ ],
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func (s *SnapSuite) TestFindSnapName(c *check.C) {
+ n := 0
+
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ q := r.URL.Query()
+ if q.Get("q") == "" {
+ v, ok := q["section"]
+ c.Check(ok, check.Equals, true)
+ c.Check(v, check.DeepEquals, []string{""})
+ }
+ fmt.Fprintln(w, findJSON)
+ default:
+ c.Fatalf("expected to get 2 requests, now on %d", n+1)
+ }
+ n++
+ })
+
+ rest, err := snap.Parser().ParseArgs([]string{"find", "hello"})
+
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary
+hello +2.10 +canonical +- +GNU Hello, the "hello world" snap
+hello-world +6.1 +canonical +- +Hello world example
+hello-huge +1.0 +noise +- +a really big snap
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+
+ s.ResetStdStreams()
+}
+
+const findHelloJSON = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": [
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "download-size": 65536,
+ "icon": "",
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "name": "hello",
+ "private": false,
+ "resource": "/v2/snaps/hello",
+ "revision": "1",
+ "status": "available",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "type": "app",
+ "version": "2.10"
+ },
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "1.0GB",
+ "developer": "noise",
+ "download-size": 512004096,
+ "icon": "",
+ "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne",
+ "name": "hello-huge",
+ "private": false,
+ "resource": "/v2/snaps/hello-huge",
+ "revision": "1",
+ "status": "available",
+ "summary": "a really big snap",
+ "type": "app",
+ "version": "1.0"
+ }
+ ],
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func (s *SnapSuite) TestFindHello(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ q := r.URL.Query()
+ c.Check(q.Get("q"), check.Equals, "hello")
+ fmt.Fprintln(w, findHelloJSON)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"find", "hello"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary
+hello +2.10 +canonical +- +GNU Hello, the "hello world" snap
+hello-huge +1.0 +noise +- +a really big snap
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const findPricedJSON = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": [
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "download-size": 65536,
+ "icon": "",
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "name": "hello",
+ "prices": {"GBP": 1.99, "USD": 2.99},
+ "private": false,
+ "resource": "/v2/snaps/hello",
+ "revision": "1",
+ "status": "priced",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "type": "app",
+ "version": "2.10"
+ }
+ ],
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func (s *SnapSuite) TestFindPriced(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ fmt.Fprintln(w, findPricedJSON)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"find", "hello"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary
+hello +2.10 +canonical +1.99GBP +GNU Hello, the "hello world" snap
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const findPricedAndBoughtJSON = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": [
+ {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "download-size": 65536,
+ "icon": "",
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "name": "hello",
+ "prices": {"GBP": 1.99, "USD": 2.99},
+ "private": false,
+ "resource": "/v2/snaps/hello",
+ "revision": "1",
+ "status": "available",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "type": "app",
+ "version": "2.10"
+ }
+ ],
+ "sources": [
+ "store"
+ ],
+ "suggested-currency": "GBP"
+}
+`
+
+func (s *SnapSuite) TestFindPricedAndBought(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ fmt.Fprintln(w, findPricedAndBoughtJSON)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"find", "hello"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary
+hello +2.10 +canonical +bought +GNU Hello, the "hello world" snap
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestFindNothingMeansFeaturedSection(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ c.Check(r.URL.Query().Get("section"), check.Equals, "featured")
+ fmt.Fprintln(w, findJSON)
+ default:
+ c.Fatalf("expected to get 1 request, now on %d", n+1)
+ }
+ n++
+ })
+
+ _, err := snap.Parser().ParseArgs([]string{"find"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestSectionCompletion(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0, 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/sections")
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": []string{"foo", "bar", "baz"},
+ })
+ default:
+ c.Fatalf("expected to get 2 requests, now on #%d", n+1)
+ }
+ n++
+ })
+
+ c.Check(snap.SectionName("").Complete(""), check.DeepEquals, []flags.Completion{
+ {Item: "foo"},
+ {Item: "bar"},
+ {Item: "baz"},
+ })
+
+ c.Check(snap.SectionName("").Complete("f"), check.DeepEquals, []flags.Completion{
+ {Item: "foo"},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdInternalFirstBoot struct{}
+
+func init() {
+ cmd := addCommand("firstboot",
+ "internal",
+ "internal", func() flags.Commander {
+ return &cmdInternalFirstBoot{}
+ }, nil, nil)
+ cmd.hidden = true
+}
+
+// WARNING: do not remove this command, older systems may still have
+// a systemd snapd.firstboot.service job in /etc/systemd/system
+// that we did not cleanup. so we need this dummy command or
+// those units will start failing.
+func (x *cmdInternalFirstBoot) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ fmt.Fprintf(Stderr, "firstboot command is deprecated")
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortGetHelp = i18n.G("Prints configuration options")
+var longGetHelp = i18n.G(`
+The get command prints configuration options for the provided snap.
+
+ $ snap get snap-name username
+ frank
+
+If multiple option names are provided, a document is returned:
+
+ $ snap get snap-name username password
+ {
+ "username": "frank",
+ "password": "..."
+ }
+
+Nested values may be retrieved via a dotted path:
+
+ $ snap get snap-name author.name
+ frank
+`)
+
+type cmdGet struct {
+ Positional struct {
+ Snap installedSnapName
+ Keys []string
+ } `positional-args:"yes" required:"yes"`
+
+ Typed bool `short:"t"`
+ Document bool `short:"d"`
+}
+
+func init() {
+ addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} },
+ map[string]string{
+ "d": i18n.G("Always return document, even with single key"),
+ "t": i18n.G("Strict typing with nulls and quoted strings"),
+ }, []argDesc{
+ {
+ name: "<snap>",
+ desc: i18n.G("The snap whose conf is being requested"),
+ },
+ {
+ name: i18n.G("<key>"),
+ desc: i18n.G("Key of interest within the configuration"),
+ },
+ })
+}
+
+func (x *cmdGet) Execute(args []string) error {
+ if len(args) > 0 {
+ // TRANSLATORS: the %s is the list of extra arguments
+ return fmt.Errorf(i18n.G("too many arguments: %s"), strings.Join(args, " "))
+ }
+
+ if x.Document && x.Typed {
+ return fmt.Errorf("cannot use -d and -t together")
+ }
+
+ snapName := string(x.Positional.Snap)
+ confKeys := x.Positional.Keys
+
+ cli := Client()
+ conf, err := cli.Conf(snapName, confKeys)
+ if err != nil {
+ return err
+ }
+
+ var confToPrint interface{} = conf
+ if !x.Document && len(confKeys) == 1 {
+ confToPrint = conf[confKeys[0]]
+ }
+
+ if x.Typed && confToPrint == nil {
+ fmt.Fprintln(Stdout, "null")
+ return nil
+ }
+
+ if s, ok := confToPrint.(string); ok && !x.Typed {
+ fmt.Fprintln(Stdout, s)
+ return nil
+ }
+
+ var bytes []byte
+ if confToPrint != nil {
+ bytes, err = json.MarshalIndent(confToPrint, "", "\t")
+ if err != nil {
+ return err
+ }
+ }
+
+ fmt.Fprintln(Stdout, string(bytes))
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ snapset "github.com/snapcore/snapd/cmd/snap"
+)
+
+var getTests = []struct {
+ args, stdout, error string
+}{{
+ args: "get snap-name --foo",
+ error: ".*unknown flag.*foo.*",
+}, {
+ args: "get snapname test-key1",
+ stdout: "test-value1\n",
+}, {
+ args: "get snapname test-key2",
+ stdout: "2\n",
+}, {
+ args: "get snapname missing-key",
+ stdout: "\n",
+}, {
+ args: "get -t snapname test-key1",
+ stdout: "\"test-value1\"\n",
+}, {
+ args: "get -t snapname test-key2",
+ stdout: "2\n",
+}, {
+ args: "get -t snapname missing-key",
+ stdout: "null\n",
+}, {
+ args: "get -d snapname test-key1",
+ stdout: "{\n\t\"test-key1\": \"test-value1\"\n}\n",
+}, {
+ args: "get snapname test-key1 test-key2",
+ stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n",
+}}
+
+func (s *SnapSuite) TestSnapGetTests(c *C) {
+ s.mockGetConfigServer(c)
+
+ for _, test := range getTests {
+ s.stdout.Truncate(0)
+ s.stderr.Truncate(0)
+
+ c.Logf("Test: %s", test.args)
+
+ _, err := snapset.Parser().ParseArgs(strings.Fields(test.args))
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ } else {
+ c.Check(err, IsNil)
+ c.Check(s.Stdout(), Equals, test.stdout)
+ }
+ }
+}
+
+func (s *SnapSuite) mockGetConfigServer(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/v2/snaps/snapname/conf" {
+ c.Errorf("unexpected path %q", r.URL.Path)
+ return
+ }
+
+ c.Check(r.Method, Equals, "GET")
+
+ query := r.URL.Query()
+ switch query.Get("keys") {
+ case "test-key1":
+ fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1"}}`)
+ case "test-key2":
+ fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key2":2}}`)
+ case "test-key1,test-key2":
+ fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1","test-key2":2}}`)
+ case "missing-key":
+ fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`)
+ default:
+ c.Errorf("unexpected keys %q", query.Get("keys"))
+ }
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "os"
+
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortHelpHelp = i18n.G("Help")
+var longHelpHelp = i18n.G(`
+The help command shows helpful information. Unlike this. ;-)
+`)
+
+type cmdHelp struct {
+ Manpage bool `long:"man"`
+ parser *flags.Parser
+}
+
+func init() {
+ addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} },
+ map[string]string{"man": i18n.G("Generate the manpage")}, nil)
+}
+
+func (cmd *cmdHelp) setParser(parser *flags.Parser) {
+ cmd.parser = parser
+}
+
+func (cmd cmdHelp) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ if cmd.Manpage {
+ cmd.parser.WriteManPage(Stdout)
+ os.Exit(0)
+ }
+
+ return &flags.Error{
+ Type: flags.ErrHelp,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "os"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestHelpPrintsHelp(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+
+ for _, cmdLine := range [][]string{
+ {"snap", "help"},
+ {"snap", "--help"},
+ {"snap", "-h"},
+ } {
+ os.Args = cmdLine
+
+ err := snap.RunMain()
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?smU)Usage:
+ +snap \[OPTIONS\] <command>
+
+Install, configure, refresh and remove snap packages. Snaps are
+'universal' packages that work across many different Linux systems,
+enabling secure distribution of the latest apps and utilities for
+cloud, servers, desktops and the internet of things.
+
+This is the CLI for snapd, a background service that takes care of
+snaps on the system. Start with 'snap list' to see installed snaps.
+
+
+Application Options:
+ +--version +Print the version and exit
+
+Help Options:
+ +-h, --help +Show this help message
+
+Available commands:
+ +abort.*
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+ }
+}
+
+func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+
+ os.Args = []string{"snap", "install", "--help"}
+
+ err := snap.RunMain()
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?smU)Usage:
+ +snap \[OPTIONS\] install \[install-OPTIONS\] <snap>...
+.*
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "path/filepath"
+ "strings"
+ "text/tabwriter"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/strutil"
+)
+
+type infoCmd struct {
+ Verbose bool `long:"verbose"`
+ Positional struct {
+ Snaps []anySnapName `positional-arg-name:"<snap>" required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+var shortInfoHelp = i18n.G("show detailed information about a snap")
+var longInfoHelp = i18n.G(`
+The info command shows detailed information about a snap, be it by name or by path.`)
+
+func init() {
+ addCommand("info",
+ shortInfoHelp,
+ longInfoHelp,
+ func() flags.Commander {
+ return &infoCmd{}
+ }, map[string]string{
+ "verbose": i18n.G("Include a verbose list of a snap's notes (otherwise, summarise notes)"),
+ }, nil)
+}
+
+func norm(path string) string {
+ path = filepath.Clean(path)
+ if osutil.IsDirectory(path) {
+ path = path + "/"
+ }
+
+ return path
+}
+
+func maybePrintType(w io.Writer, t string) {
+ // XXX: using literals here until we reshuffle snap & client properly
+ // (and os->core rename happens, etc)
+ switch t {
+ case "", "app", "application":
+ return
+ case "os":
+ t = "core"
+ }
+
+ fmt.Fprintf(w, "type:\t%s\n", t)
+}
+
+func tryDirect(w io.Writer, path string, verbose bool) bool {
+ path = norm(path)
+
+ snapf, err := snap.Open(path)
+ if err != nil {
+ return false
+ }
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ if err != nil {
+ return false
+ }
+ fmt.Fprintf(w, "path:\t%q\n", path)
+ fmt.Fprintf(w, "name:\t%s\n", info.Name())
+ fmt.Fprintf(w, "summary:\t%q\n", info.Summary())
+
+ var notes *Notes
+ if verbose {
+ fmt.Fprintln(w, "notes:\t")
+ fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement)
+ if info.Broken == "" {
+ fmt.Fprintln(w, " broken:\tfalse")
+ } else {
+ fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken)
+ }
+
+ } else {
+ notes = NotesFromInfo(info)
+ }
+ fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes)
+ maybePrintType(w, string(info.Type))
+
+ return true
+}
+
+func coalesce(snaps ...*client.Snap) *client.Snap {
+ for _, s := range snaps {
+ if s != nil {
+ return s
+ }
+ }
+ return nil
+}
+
+// formatDescr formats a given string (typically a snap description)
+// in a user friendly way.
+//
+// The rules are (intentionally) very simple:
+// - word wrap at "max" chars
+// - keep \n intact and break here
+// - ignore \r
+func formatDescr(descr string, max int) string {
+ out := bytes.NewBuffer(nil)
+ for _, line := range strings.Split(descr, "\n") {
+ if len(line) > max {
+ for _, chunk := range strutil.WordWrap(line, max) {
+ fmt.Fprintf(out, " %s\n", chunk)
+ }
+ } else {
+ fmt.Fprintf(out, " %s\n", line)
+ }
+ }
+
+ return strings.TrimSuffix(out.String(), "\n")
+}
+
+func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) {
+ if len(allApps) == 0 {
+ return
+ }
+
+ commands := make([]string, 0, len(allApps))
+ for _, app := range allApps {
+ if app.Daemon != "" {
+ continue
+ }
+
+ // TODO: helper for this?
+ cmdStr := app.Name
+ if cmdStr != snapName {
+ cmdStr = fmt.Sprintf("%s.%s", snapName, cmdStr)
+ }
+
+ if len(app.Aliases) != 0 {
+ cmdStr = fmt.Sprintf("%s (%s)", cmdStr, strings.Join(app.Aliases, ","))
+ }
+
+ commands = append(commands, cmdStr)
+ }
+ if len(commands) == 0 {
+ return
+ }
+
+ fmt.Fprintf(w, "commands:\n")
+ for _, cmd := range commands {
+ fmt.Fprintf(w, " - %s\n", cmd)
+ }
+}
+
+func (x *infoCmd) Execute([]string) error {
+ cli := Client()
+
+ w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0)
+
+ noneOK := true
+ for i, snapName := range x.Positional.Snaps {
+ snapName := string(snapName)
+ if i > 0 {
+ fmt.Fprintln(w, "---")
+ }
+
+ if tryDirect(w, snapName, x.Verbose) {
+ noneOK = false
+ continue
+ }
+ remote, _, _ := cli.FindOne(snapName)
+ local, _, _ := cli.Snap(snapName)
+
+ both := coalesce(local, remote)
+
+ if both == nil {
+ fmt.Fprintf(w, "argument:\t%q\nwarning:\t%s\n", snapName, i18n.G("not a valid snap"))
+ continue
+ }
+ noneOK = false
+
+ fmt.Fprintf(w, "name:\t%s\n", both.Name)
+ fmt.Fprintf(w, "summary:\t%q\n", both.Summary)
+ // TODO: have publisher; use publisher here,
+ // and additionally print developer if publisher != developer
+ fmt.Fprintf(w, "publisher:\t%s\n", both.Developer)
+ // FIXME: find out for real
+ termWidth := 77
+ fmt.Fprintf(w, "description: |\n%s\n", formatDescr(both.Description, termWidth))
+ maybePrintType(w, both.Type)
+ maybePrintCommands(w, snapName, both.Apps, termWidth)
+ if x.Verbose {
+ fmt.Fprintln(w, "notes:\t")
+ fmt.Fprintf(w, " private:\t%t\n", both.Private)
+ fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement)
+ }
+
+ if local != nil {
+ var notes *Notes
+ if x.Verbose {
+ jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode
+ fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode)
+ fmt.Fprintf(w, " jailmode:\t%t\n", jailMode)
+ fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode)
+ fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive)
+ if local.Broken == "" {
+ fmt.Fprintf(w, " broken:\t%t\n", false)
+ } else {
+ fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken)
+ }
+ } else {
+ notes = NotesFromLocal(local)
+ }
+
+ fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel)
+ fmt.Fprintf(w, "installed:\t%s\t(%s)\t%s\t%s\n", local.Version, local.Revision, strutil.SizeToStr(local.InstalledSize), notes)
+ fmt.Fprintf(w, "refreshed:\t%s\n", local.InstallDate)
+ }
+
+ if remote != nil && remote.Channels != nil {
+ // \t\t\t so we get "installed" lined up with "channels"
+ fmt.Fprintf(w, "channels:\t\t\t\n")
+ for _, ch := range []string{"stable", "candidate", "beta", "edge"} {
+ m := remote.Channels[ch]
+ if m == nil {
+ continue
+ }
+ fmt.Fprintf(w, " %s:\t%s\t(%s)\t%s\t%s\n", ch, m.Version, m.Revision, strutil.SizeToStr(m.Size), NotesFromChannelSnapInfo(m))
+ }
+ }
+ }
+ w.Flush()
+
+ if noneOK {
+ return fmt.Errorf(i18n.G("no valid snaps given"))
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdInterfaces struct {
+ Interface string `short:"i"`
+ Positionals struct {
+ Query SnapAndName `skip-help:"true"`
+ } `positional-args:"true"`
+}
+
+var shortInterfacesHelp = i18n.G("Lists interfaces in the system")
+var longInterfacesHelp = i18n.G(`
+The interfaces command lists interfaces available in the system.
+
+By default all slots and plugs, used and offered by all snaps, are displayed.
+
+$ snap interfaces <snap>:<slot or plug>
+
+Lists only the specified slot or plug.
+
+$ snap interfaces <snap>
+
+Lists the slots offered and plugs used by the specified snap.
+
+$ snap interfaces -i=<interface> [<snap>]
+
+Filters the complete output so only plugs and/or slots matching the provided details are listed.
+`)
+
+func init() {
+ addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander {
+ return &cmdInterfaces{}
+ }, map[string]string{
+ "i": i18n.G("Constrain listing to specific interfaces"),
+ }, []argDesc{{
+ name: i18n.G("<snap>:<slot or plug>"),
+ desc: i18n.G("Constrain listing to a specific snap or snap:name"),
+ }})
+}
+
+func (x *cmdInterfaces) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ ifaces, err := Client().Interfaces()
+ if err == nil {
+ if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 {
+ return fmt.Errorf(i18n.G("no interfaces found"))
+ }
+ w := tabWriter()
+ fmt.Fprintln(w, i18n.G("Slot\tPlug"))
+ defer w.Flush()
+ for _, slot := range ifaces.Slots {
+ if wanted := x.Positionals.Query.Snap; wanted != "" {
+ ok := wanted == slot.Snap
+ for i := 0; i < len(slot.Connections) && !ok; i++ {
+ ok = wanted == slot.Connections[i].Snap
+ }
+ if !ok {
+ continue
+ }
+ }
+ if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != slot.Name {
+ continue
+ }
+ if x.Interface != "" && slot.Interface != x.Interface {
+ continue
+ }
+ // The OS snap is special and enable abbreviated
+ // display syntax on the slot-side of the connection.
+ if slot.Snap == "core" || slot.Snap == "ubuntu-core" {
+ fmt.Fprintf(w, ":%s\t", slot.Name)
+ } else {
+ fmt.Fprintf(w, "%s:%s\t", slot.Snap, slot.Name)
+ }
+ for i := 0; i < len(slot.Connections); i++ {
+ if i > 0 {
+ fmt.Fprint(w, ",")
+ }
+ if slot.Connections[i].Name != slot.Name {
+ fmt.Fprintf(w, "%s:%s", slot.Connections[i].Snap, slot.Connections[i].Name)
+ } else {
+ fmt.Fprintf(w, "%s", slot.Connections[i].Snap)
+ }
+ }
+ // Display visual indicator for disconnected slots
+ if len(slot.Connections) == 0 {
+ fmt.Fprint(w, "-")
+ }
+ fmt.Fprintf(w, "\n")
+ }
+ // Plugs are treated differently. Since the loop above already printed each connected
+ // plug, the loop below focuses on printing just the disconnected plugs.
+ for _, plug := range ifaces.Plugs {
+ if x.Positionals.Query.Snap != "" && x.Positionals.Query.Snap != plug.Snap {
+ continue
+ }
+ if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != plug.Name {
+ continue
+ }
+ if x.Interface != "" && plug.Interface != x.Interface {
+ continue
+ }
+ // Display visual indicator for disconnected plugs.
+ if len(plug.Connections) == 0 {
+ fmt.Fprintf(w, "-\t%s:%s\n", plug.Snap, plug.Name)
+ }
+ }
+ }
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "io/ioutil"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestInterfacesHelp(c *C) {
+ msg := `Usage:
+ snap.test [OPTIONS] interfaces [interfaces-OPTIONS] [<snap>:<slot or plug>]
+
+The interfaces command lists interfaces available in the system.
+
+By default all slots and plugs, used and offered by all snaps, are displayed.
+
+$ snap interfaces <snap>:<slot or plug>
+
+Lists only the specified slot or plug.
+
+$ snap interfaces <snap>
+
+Lists the slots offered and plugs used by the specified snap.
+
+$ snap interfaces -i=<interface> [<snap>]
+
+Filters the complete output so only plugs and/or slots matching the provided
+details are listed.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+
+[interfaces command options]
+ -i= Constrain listing to specific interfaces
+
+[interfaces command arguments]
+ <snap>:<slot or plug>: Constrain listing to a specific snap or snap:name
+`
+ rest, err := Parser().ParseArgs([]string{"interfaces", "--help"})
+ c.Assert(err.Error(), Equals, msg)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestInterfacesZeroSlotsOnePlug(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "- keyboard-lights:capslock-led\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesZeroPlugsOneSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "canonical-pi2:pin-13 -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesOneSlotOnePlug(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.PlugRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "bool-file",
+ Label: "Capslock indicator LED",
+ Connections: []client.SlotRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ },
+ },
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "canonical-pi2:pin-13 keyboard-lights:capslock-led\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.SetUpTest(c)
+ // should be the same
+ rest, err = Parser().ParseArgs([]string{"interfaces", "canonical-pi2"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.SetUpTest(c)
+ // and the same again
+ rest, err = Parser().ParseArgs([]string{"interfaces", "keyboard-lights"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesTwoPlugs(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.PlugRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ {
+ Snap: "keyboard-lights",
+ Name: "scrollock-led",
+ },
+ },
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "canonical-pi2:pin-13 keyboard-lights:capslock-led,keyboard-lights:scrollock-led\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesPlugsWithCommonName(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "canonical-pi2",
+ Name: "network-listening",
+ Interface: "network-listening",
+ Label: "Ability to be a network service",
+ Connections: []client.PlugRef{
+ {
+ Snap: "paste-daemon",
+ Name: "network-listening",
+ },
+ {
+ Snap: "time-daemon",
+ Name: "network-listening",
+ },
+ },
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "paste-daemon",
+ Name: "network-listening",
+ Interface: "network-listening",
+ Label: "Ability to be a network service",
+ Connections: []client.SlotRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "network-listening",
+ },
+ },
+ },
+ {
+ Snap: "time-daemon",
+ Name: "network-listening",
+ Interface: "network-listening",
+ Label: "Ability to be a network service",
+ Connections: []client.SlotRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "network-listening",
+ },
+ },
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "canonical-pi2:network-listening paste-daemon,time-daemon\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesOsSnapSlots(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "core",
+ Name: "network-listening",
+ Interface: "network-listening",
+ Label: "Ability to be a network service",
+ Connections: []client.PlugRef{
+ {
+ Snap: "paste-daemon",
+ Name: "network-listening",
+ },
+ {
+ Snap: "time-daemon",
+ Name: "network-listening",
+ },
+ },
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "paste-daemon",
+ Name: "network-listening",
+ Interface: "network-listening",
+ Label: "Ability to be a network service",
+ Connections: []client.SlotRef{
+ {
+ Snap: "core",
+ Name: "network-listening",
+ },
+ },
+ },
+ {
+ Snap: "time-daemon",
+ Name: "network-listening",
+ Interface: "network-listening",
+ Label: "Ability to be a network service",
+ Connections: []client.SlotRef{
+ {
+ Snap: "core",
+ Name: "network-listening",
+ },
+ },
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ ":network-listening paste-daemon,time-daemon\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesTwoSlotsAndFiltering(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "canonical-pi2",
+ Name: "debug-console",
+ Interface: "serial-port",
+ Label: "Serial port on the expansion header",
+ Connections: []client.PlugRef{
+ {
+ Snap: "core",
+ Name: "debug-console",
+ },
+ },
+ },
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.PlugRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces", "-i=serial-port"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "canonical-pi2:debug-console core\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesOfSpecificSnap(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "cheese",
+ Name: "photo-trigger",
+ Interface: "bool-file",
+ Label: "Photo trigger",
+ },
+ {
+ Snap: "wake-up-alarm",
+ Name: "toggle",
+ Interface: "bool-file",
+ Label: "Alarm toggle",
+ },
+ {
+ Snap: "wake-up-alarm",
+ Name: "snooze",
+ Interface: "bool-file",
+ Label: "Alarm snooze",
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "wake-up-alarm:toggle -\n" +
+ "wake-up-alarm:snooze -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesOfSpecificSnapAndSlot(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{
+ Slots: []client.Slot{
+ {
+ Snap: "cheese",
+ Name: "photo-trigger",
+ Interface: "bool-file",
+ Label: "Photo trigger",
+ },
+ {
+ Snap: "wake-up-alarm",
+ Name: "toggle",
+ Interface: "bool-file",
+ Label: "Alarm toggle",
+ },
+ {
+ Snap: "wake-up-alarm",
+ Name: "snooze",
+ Interface: "bool-file",
+ Label: "Alarm snooze",
+ },
+ },
+ },
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm:snooze"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Slot Plug\n" +
+ "wake-up-alarm:snooze -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestInterfacesNothingAtAll(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": client.Interfaces{},
+ })
+ })
+ rest, err := Parser().ParseArgs([]string{"interfaces"})
+ c.Assert(err, ErrorMatches, "no interfaces found")
+ // XXX: not sure why this is returned, I guess that's what happens when a
+ // command Execute returns an error.
+ c.Assert(rest, DeepEquals, []string{"interfaces"})
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdKeys struct {
+ JSON bool `long:"json"`
+}
+
+func init() {
+ cmd := addCommand("keys",
+ i18n.G("List cryptographic keys"),
+ i18n.G("List cryptographic keys that can be used for signing assertions."),
+ func() flags.Commander {
+ return &cmdKeys{}
+ }, map[string]string{"json": i18n.G("Output results in JSON format")}, nil)
+ cmd.hidden = true
+}
+
+// Key represents a key that can be used for signing assertions.
+type Key struct {
+ Name string `json:"name"`
+ Sha3_384 string `json:"sha3-384"`
+}
+
+func (x *cmdKeys) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ w := tabWriter()
+ if !x.JSON {
+ fmt.Fprintln(w, i18n.G("Name\tSHA3-384"))
+ defer w.Flush()
+ }
+ keys := []Key{}
+
+ manager := asserts.NewGPGKeypairManager()
+ display := func(privk asserts.PrivateKey, fpr string, uid string) error {
+ key := Key{
+ Name: uid,
+ Sha3_384: privk.PublicKey().ID(),
+ }
+ if x.JSON {
+ keys = append(keys, key)
+ } else {
+ fmt.Fprintf(w, "%s\t%s\n", key.Name, key.Sha3_384)
+ }
+ return nil
+ }
+ err := manager.Walk(display)
+ if err != nil {
+ return err
+ }
+ if x.JSON {
+ obj, err := json.Marshal(keys)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(Stdout, "%s\n", obj)
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+type SnapKeysSuite struct {
+ BaseSnapSuite
+
+ GnupgCmd string
+}
+
+// FIXME: Ideally we would just use gpg2 and remove the gnupg2_test.go file.
+// However currently there is LP: #1621839 which prevents us from
+// switching to gpg2 fully. Once this is resolved we should switch.
+var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg"})
+
+var fakePinentryData = []byte(`#!/bin/sh
+set -e
+echo "OK Pleased to meet you"
+while true; do
+ read line
+ case $line in
+ BYE)
+ exit 0
+ ;;
+ *)
+ echo "OK I agree to everything"
+ ;;
+esac
+done
+`)
+
+func (s *SnapKeysSuite) SetUpTest(c *C) {
+ s.BaseSnapSuite.SetUpTest(c)
+
+ tempdir := c.MkDir()
+ for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} {
+ data, err := ioutil.ReadFile(filepath.Join("test-data", fileName))
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644)
+ c.Assert(err, IsNil)
+ }
+ fakePinentryFn := filepath.Join(tempdir, "pinentry-fake")
+ err := ioutil.WriteFile(fakePinentryFn, fakePinentryData, 0755)
+ c.Assert(err, IsNil)
+ gpgAgentConfFn := filepath.Join(tempdir, "gpg-agent.conf")
+ err = ioutil.WriteFile(gpgAgentConfFn, []byte(fmt.Sprintf(`pinentry-program %s`, fakePinentryFn)), 0644)
+ c.Assert(err, IsNil)
+
+ os.Setenv("SNAP_GNUPG_HOME", tempdir)
+ os.Setenv("SNAP_GNUPG_CMD", s.GnupgCmd)
+}
+
+func (s *SnapKeysSuite) TearDownTest(c *C) {
+ os.Unsetenv("SNAP_GNUPG_HOME")
+ os.Unsetenv("SNAP_GNUPG_CMD")
+ s.BaseSnapSuite.TearDownTest(c)
+}
+
+func (s *SnapKeysSuite) TestKeys(c *C) {
+ rest, err := snap.Parser().ParseArgs([]string{"keys"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches, `Name +SHA3-384
+default +g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ
+another +DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L
+`)
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestKeysJSON(c *C) {
+ rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedResponse := []snap.Key{
+ {
+ Name: "default",
+ Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ",
+ },
+ {
+ Name: "another",
+ Sha3_384: "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L",
+ },
+ }
+ var obtainedResponse []snap.Key
+ json.Unmarshal(s.stdout.Bytes(), &obtainedResponse)
+ c.Check(obtainedResponse, DeepEquals, expectedResponse)
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapKeysSuite) TestKeysJSONEmpty(c *C) {
+ err := os.RemoveAll(os.Getenv("SNAP_GNUPG_HOME"))
+ c.Assert(err, IsNil)
+ rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "[]\n")
+ c.Check(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/store"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdKnown struct {
+ KnownOptions struct {
+ // XXX: how to get a list of assert types for completion?
+ AssertTypeName string `required:"true"`
+ HeaderFilters []string `required:"0"`
+ } `positional-args:"true" required:"true"`
+
+ Remote bool `long:"remote"`
+}
+
+var shortKnownHelp = i18n.G("Shows known assertions of the provided type")
+var longKnownHelp = i18n.G(`
+The known command shows known assertions of the provided type.
+If header=value pairs are provided after the assertion type, the assertions
+shown must also have the specified headers matching the provided values.
+`)
+
+func init() {
+ addCommand("known", shortKnownHelp, longKnownHelp, func() flags.Commander {
+ return &cmdKnown{}
+ }, nil, []argDesc{
+ {
+ name: i18n.G("<assertion type>"),
+ desc: i18n.G("Assertion type name"),
+ }, {
+ name: i18n.G("<header filter>"),
+ desc: i18n.G("Constrain listing to those matching header=value"),
+ },
+ })
+}
+
+var nl = []byte{'\n'}
+
+var storeNew = store.New
+
+func downloadAssertion(typeName string, headers map[string]string) ([]asserts.Assertion, error) {
+ var user *auth.UserState
+
+ // FIXME: set auth context
+ var authContext auth.AuthContext
+
+ at := asserts.Type(typeName)
+ if at == nil {
+ return nil, fmt.Errorf("cannot find assertion type %q", typeName)
+ }
+ primaryKeys := make([]string, len(at.PrimaryKey))
+ for i, k := range at.PrimaryKey {
+ pk, ok := headers[k]
+ if !ok {
+ return nil, fmt.Errorf("missing primary header %q to query remote assertion", k)
+ }
+ primaryKeys[i] = pk
+ }
+
+ sto := storeNew(nil, authContext)
+ as, err := sto.Assertion(at, primaryKeys, user)
+ if err != nil {
+ return nil, err
+ }
+
+ return []asserts.Assertion{as}, nil
+}
+
+func (x *cmdKnown) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ // TODO: share this kind of parsing once it's clearer how often is used in snap
+ headers := map[string]string{}
+ for _, headerFilter := range x.KnownOptions.HeaderFilters {
+ parts := strings.SplitN(headerFilter, "=", 2)
+ if len(parts) != 2 {
+ return fmt.Errorf(i18n.G("invalid header filter: %q (want key=value)"), headerFilter)
+ }
+ headers[parts[0]] = parts[1]
+ }
+
+ var assertions []asserts.Assertion
+ var err error
+ if x.Remote {
+ assertions, err = downloadAssertion(x.KnownOptions.AssertTypeName, headers)
+ } else {
+ assertions, err = Client().Known(x.KnownOptions.AssertTypeName, headers)
+ }
+ if err != nil {
+ return err
+ }
+
+ enc := asserts.NewEncoder(Stdout)
+ for _, a := range assertions {
+ enc.Encode(a)
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/store"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+// acquire example data via:
+// curl -H "accept: application/x.ubuntu.assertion" https://assertions.ubuntu.com/v1/assertions/model/16/canonical/pi2
+const mockModelAssertion = `type: model
+authority-id: canonical
+series: 16
+brand-id: canonical
+model: pi99
+architecture: armhf
+gadget: pi99
+kernel: pi99-kernel
+timestamp: 2016-08-31T00:00:00.0Z
+sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
+
+AcLorsomethingthatlooksvaguelylikeasignature==
+`
+
+func (s *SnapSuite) TestKnownRemote(c *check.C) {
+ var server *httptest.Server
+
+ restorer := snap.MockStoreNew(func(cfg *store.Config, auth auth.AuthContext) *store.Store {
+ if cfg == nil {
+ cfg = store.DefaultConfig()
+ }
+ serverURL, err := url.Parse(server.URL + "/assertions/")
+ c.Assert(err, check.IsNil)
+ cfg.AssertionsURI = serverURL
+ return store.New(cfg, auth)
+ })
+ defer restorer()
+
+ n := 0
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/assertions/model/16/canonical/pi99")
+ fmt.Fprintln(w, mockModelAssertion)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ }))
+
+ rest, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, mockModelAssertion)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestKnownRemoteMissingPrimaryKey(c *check.C) {
+ _, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"})
+ c.Assert(err, check.ErrorMatches, `missing primary header "model" to query remote assertion`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "sort"
+ "text/tabwriter"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortListHelp = i18n.G("List installed snaps")
+var longListHelp = i18n.G(`
+The list command displays a summary of snaps installed in the current system.`)
+
+type cmdList struct {
+ Positional struct {
+ Snaps []installedSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"yes"`
+
+ All bool `long:"all"`
+}
+
+func init() {
+ addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} },
+ map[string]string{"all": i18n.G("Show all revisions")}, nil)
+}
+
+type snapsByName []*client.Snap
+
+func (s snapsByName) Len() int { return len(s) }
+func (s snapsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
+func (s snapsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+func (x *cmdList) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ names := make([]string, len(x.Positional.Snaps))
+ for i, name := range x.Positional.Snaps {
+ names[i] = string(name)
+ }
+
+ return listSnaps(names, x.All)
+}
+
+func listSnaps(names []string, all bool) error {
+ cli := Client()
+ snaps, err := cli.List(names, &client.ListOptions{All: all})
+ if err != nil {
+ if err == client.ErrNoSnapsInstalled {
+ fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try \"snap install hello-world\"."))
+ return nil
+ }
+ return err
+ } else if len(snaps) == 0 {
+ return errors.New(i18n.G("no matching snaps installed"))
+ }
+ sort.Sort(snapsByName(snaps))
+
+ w := tabWriter()
+ defer w.Flush()
+
+ fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes"))
+
+ for _, snap := range snaps {
+ // TODO: make JailMode a flag in the snap itself
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromLocal(snap))
+ }
+
+ return nil
+}
+
+func tabWriter() *tabwriter.Writer {
+ return tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestListHelp(c *check.C) {
+ msg := `Usage:
+ snap.test [OPTIONS] list [list-OPTIONS] [<snap>...]
+
+The list command displays a summary of snaps installed in the current system.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+
+[list command options]
+ --all Show all revisions
+`
+ rest, err := snap.Parser().ParseArgs([]string{"list", "--help"})
+ c.Assert(err.Error(), check.Equals, msg)
+ c.Assert(rest, check.DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestList(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(r.URL.RawQuery, check.Equals, "")
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"list"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes
+foo +4.2 +17 +bar +-
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestListAll(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(r.URL.RawQuery, check.Equals, "select=all")
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"list", "--all"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes
+foo +4.2 +17 +bar +-
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestListEmpty(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ fmt.Fprintln(w, `{"type": "sync", "result": []}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"list"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n")
+}
+
+func (s *SnapSuite) TestListEmptyWithQuery(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ fmt.Fprintln(w, `{"type": "sync", "result": []}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"list", "quux"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n")
+}
+
+func (s *SnapSuite) TestListWithNoMatchingQuery(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ _, err := snap.Parser().ParseArgs([]string{"list", "quux"})
+ c.Assert(err, check.ErrorMatches, "no matching snaps installed")
+}
+
+func (s *SnapSuite) TestListWithQuery(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(r.URL.Query(), check.HasLen, 0)
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"list", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes
+foo +4.2 +17 +bar +-
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestListWithNotes(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ fmt.Fprintln(w, `{"type": "sync", "result": [
+{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "trymode": true}
+,{"name": "dm1", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "devmode"}
+,{"name": "dm2", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "strict"}
+,{"name": "cf1", "status": "active", "version": "6", "revision":2, "confinement": "devmode", "jailmode": true}
+]}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"list"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?ms)^Name +Version +Rev +Developer +Notes$`)
+ c.Check(s.Stdout(), check.Matches, `(?ms).*^foo +4.2 +17 +bar +try$`)
+ c.Check(s.Stdout(), check.Matches, `(?ms).*^dm1 +.* +devmode$`)
+ c.Check(s.Stdout(), check.Matches, `(?ms).*^dm2 +.* +devmode$`)
+ c.Check(s.Stdout(), check.Matches, `(?ms).*^cf1 +.* +jailmode$`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdLogin struct {
+ Positional struct {
+ Email string
+ } `positional-args:"yes"`
+}
+
+var shortLoginHelp = i18n.G("Authenticates on snapd and the store")
+
+var longLoginHelp = i18n.G(`
+The login command authenticates on snapd and the snap store and saves credentials
+into the ~/.snap/auth.json file. Further communication with snapd will then be made
+using those credentials.
+
+Login only works for local users in the sudo, admin or wheel groups.
+
+An account can be setup at https://login.ubuntu.com
+`)
+
+func init() {
+ addCommand("login",
+ shortLoginHelp,
+ longLoginHelp,
+ func() flags.Commander {
+ return &cmdLogin{}
+ }, nil, []argDesc{{
+ // TRANSLATORS: noun
+ name: i18n.G("<email>"),
+ desc: i18n.G("The login.ubuntu.com email to login as"),
+ }})
+}
+
+func requestLoginWith2faRetry(email, password string) error {
+ var otp []byte
+ var err error
+
+ var msgs = [3]string{
+ i18n.G("Two-factor code: "),
+ i18n.G("Bad code. Try again: "),
+ i18n.G("Wrong again. Once more: "),
+ }
+
+ cli := Client()
+ reader := bufio.NewReader(nil)
+
+ for i := 0; ; i++ {
+ // first try is without otp
+ _, err = cli.Login(email, password, string(otp))
+ if i >= len(msgs) || !client.IsTwoFactorError(err) {
+ return err
+ }
+
+ reader.Reset(Stdin)
+ fmt.Fprint(Stdout, msgs[i])
+ // the browser shows it as well (and Sergio wants to see it ;)
+ otp, _, err = reader.ReadLine()
+ if err != nil {
+ return err
+ }
+ }
+}
+
+func requestLogin(email string) error {
+ fmt.Fprint(Stdout, fmt.Sprintf(i18n.G("Password of %q: "), email))
+ password, err := ReadPassword(0)
+ fmt.Fprint(Stdout, "\n")
+ if err != nil {
+ return err
+ }
+
+ // strings.TrimSpace needed because we get \r from the pty in the tests
+ return requestLoginWith2faRetry(email, strings.TrimSpace(string(password)))
+}
+
+func (x *cmdLogin) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ email := x.Positional.Email
+ if email == "" {
+ fmt.Fprint(Stdout, i18n.G("Email address: "))
+ in, _, err := bufio.NewReader(Stdin).ReadLine()
+ if err != nil {
+ return err
+ }
+ email = string(in)
+ }
+
+ err := requestLogin(email)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(Stdout, i18n.G("Login successful"))
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+var mockLoginRsp = `{"type": "sync", "result": {"id":42, "username": "foo", "email": "foo@example.com", "macaroon": "yummy", "discarages":"plenty"}}`
+
+func makeLoginTestServer(c *C, n *int) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ switch *n {
+ case 0:
+ c.Check(r.URL.Path, Equals, "/v2/login")
+ c.Check(r.Method, Equals, "POST")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(postData), Equals, `{"email":"foo@example.com","password":"some-password"}`+"\n")
+ fmt.Fprintln(w, mockLoginRsp)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ *n++
+ }
+}
+
+func (s *SnapSuite) TestLoginSimple(c *C) {
+ n := 0
+ s.RedirectClientToTestServer(makeLoginTestServer(c, &n))
+
+ // send the password
+ s.password = "some-password\n"
+ rest, err := snap.Parser().ParseArgs([]string{"login", "foo@example.com"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, `Password of "foo@example.com":
+Login successful
+`)
+ c.Check(s.Stderr(), Equals, "")
+ c.Check(n, Equals, 1)
+}
+
+func (s *SnapSuite) TestLoginAskEmail(c *C) {
+ n := 0
+ s.RedirectClientToTestServer(makeLoginTestServer(c, &n))
+
+ // send the email
+ fmt.Fprint(s.stdin, "foo@example.com\n")
+ // send the password
+ s.password = "some-password"
+
+ rest, err := snap.Parser().ParseArgs([]string{"login"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ // test slightly ugly, on a real system STDOUT will be:
+ // Email address: foo@example.com\n
+ // because the input to stdin is echoed
+ c.Check(s.Stdout(), Equals, `Email address: Password of "foo@example.com":
+Login successful
+`)
+ c.Check(s.Stderr(), Equals, "")
+ c.Check(n, Equals, 1)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdLogout struct{}
+
+var shortLogoutHelp = i18n.G("Log out of the store")
+
+var longLogoutHelp = i18n.G("This command logs the current user out of the store")
+
+func init() {
+ addCommand("logout",
+ shortLogoutHelp,
+ longLogoutHelp,
+ func() flags.Commander {
+ return &cmdLogout{}
+ }, nil, nil)
+}
+
+func (cmd *cmdLogout) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ return Client().Logout()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortIsManagedHelp = i18n.G("Prints whether system is managed")
+var longIsManagedHelp = i18n.G(`
+The managed command will print true or false informing whether
+snapd has registered users.
+`)
+
+type cmdIsManaged struct{}
+
+func init() {
+ cmd := addCommand("managed", shortIsManagedHelp, longIsManagedHelp, func() flags.Commander { return &cmdIsManaged{} }, nil, nil)
+ cmd.hidden = true
+}
+
+func (cmd cmdIsManaged) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ sysinfo, err := Client().SysInfo()
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(Stdout, "%v\n", sysinfo.Managed)
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestManaged(c *C) {
+ for _, managed := range []bool{true, false} {
+ s.stdout.Truncate(0)
+
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/system-info")
+
+ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {"managed":%v}}`, managed)
+ })
+
+ _, err := snap.Parser().ParseArgs([]string{"managed"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", managed))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "path/filepath"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/image"
+)
+
+type cmdPrepareImage struct {
+ Positional struct {
+ ModelAssertionFn string
+ Rootdir string
+ } `positional-args:"yes" required:"yes"`
+
+ ExtraSnaps []string `long:"extra-snaps"`
+ Channel string `long:"channel"`
+}
+
+func init() {
+ cmd := addCommand("prepare-image",
+ i18n.G("Prepare a snappy image"),
+ i18n.G("Prepare a snappy image"),
+ func() flags.Commander {
+ return &cmdPrepareImage{}
+ }, map[string]string{
+ "extra-snaps": "Extra snaps to be installed",
+ "channel": "The channel to use",
+ }, []argDesc{
+ {
+ name: i18n.G("<model-assertion>"),
+ desc: i18n.G("The model assertion name"),
+ }, {
+ name: i18n.G("<root-dir>"),
+ desc: i18n.G("The output directory"),
+ },
+ })
+ cmd.hidden = true
+}
+
+func (x *cmdPrepareImage) Execute(args []string) error {
+ opts := &image.Options{
+ ModelFile: x.Positional.ModelAssertionFn,
+
+ RootDir: filepath.Join(x.Positional.Rootdir, "image"),
+ GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"),
+ Channel: x.Channel,
+ Snaps: x.ExtraSnaps,
+ }
+
+ return image.Prepare(opts)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strings"
+ "syscall"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snapenv"
+)
+
+var (
+ syscallExec = syscall.Exec
+ userCurrent = user.Current
+)
+
+type cmdRun struct {
+ Command string `long:"command" hidden:"yes"`
+ Hook string `long:"hook" hidden:"yes"`
+ Revision string `short:"r" default:"unset" hidden:"yes"`
+ Shell bool `long:"shell" `
+}
+
+func init() {
+ addCommand("run",
+ i18n.G("Run the given snap command"),
+ i18n.G("Run the given snap command with the right confinement and environment"),
+ func() flags.Commander {
+ return &cmdRun{}
+ }, map[string]string{
+ "command": i18n.G("Alternative command to run"),
+ "hook": i18n.G("Hook to run"),
+ "r": i18n.G("Use a specific snap revision when running hook"),
+ "shell": i18n.G("Run a shell instead of the command (useful for debugging)"),
+ }, nil)
+}
+
+func (x *cmdRun) Execute(args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf(i18n.G("need the application to run as argument"))
+ }
+ snapApp := args[0]
+ args = args[1:]
+
+ // Catch some invalid parameter combinations, provide helpful errors
+ if x.Hook != "" && x.Command != "" {
+ return fmt.Errorf(i18n.G("cannot use --hook and --command together"))
+ }
+ if x.Revision != "unset" && x.Revision != "" && x.Hook == "" {
+ return fmt.Errorf(i18n.G("-r can only be used with --hook"))
+ }
+ if x.Hook != "" && len(args) > 0 {
+ // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments
+ return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.Hook, strings.Join(args, " "))
+ }
+
+ // Now actually handle the dispatching
+ if x.Hook != "" {
+ return snapRunHook(snapApp, x.Revision, x.Hook)
+ }
+
+ // pass shell as a special command to snap-exec
+ if x.Shell {
+ x.Command = "shell"
+ }
+
+ return snapRunApp(snapApp, x.Command, args)
+}
+
+func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) {
+ if revision.Unset() {
+ curFn := filepath.Join(dirs.SnapMountDir, snapName, "current")
+ realFn, err := os.Readlink(curFn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err)
+ }
+ rev := filepath.Base(realFn)
+ revision, err = snap.ParseRevision(rev)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read revision %s: %s", rev, err)
+ }
+ }
+
+ info, err := snap.ReadInfo(snapName, &snap.SideInfo{
+ Revision: revision,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
+
+func createUserDataDirs(info *snap.Info) error {
+ usr, err := userCurrent()
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
+ }
+
+ // see snapenv.User
+ userData := info.UserDataDir(usr.HomeDir)
+ commonUserData := info.UserCommonDataDir(usr.HomeDir)
+ for _, d := range []string{userData, commonUserData} {
+ if err := os.MkdirAll(d, 0755); err != nil {
+ // TRANSLATORS: %q is the directory whose creation failed, %v the error message
+ return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err)
+ }
+ }
+ return nil
+}
+
+func snapRunApp(snapApp, command string, args []string) error {
+ snapName, appName := snap.SplitSnapApp(snapApp)
+ info, err := getSnapInfo(snapName, snap.R(0))
+ if err != nil {
+ return err
+ }
+
+ app := info.Apps[appName]
+ if app == nil {
+ return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName)
+ }
+
+ return runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args)
+}
+
+func snapRunHook(snapName, snapRevision, hookName string) error {
+ revision, err := snap.ParseRevision(snapRevision)
+ if err != nil {
+ return err
+ }
+
+ info, err := getSnapInfo(snapName, revision)
+ if err != nil {
+ return err
+ }
+
+ hook := info.Hooks[hookName]
+ if hook == nil {
+ return fmt.Errorf(i18n.G("cannot find hook %q in %q"), hookName, snapName)
+ }
+
+ return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil)
+}
+
+func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error {
+ if err := createUserDataDirs(info); err != nil {
+ logger.Noticef("WARNING: cannot create user data directory: %s", err)
+ }
+
+ cmd := []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"),
+ }
+ if info.NeedsClassic() {
+ cmd = append(cmd, "--classic")
+ }
+ cmd = append(cmd, securityTag)
+ cmd = append(cmd, filepath.Join(dirs.LibExecDir, "snap-exec"))
+
+ if command != "" {
+ cmd = append(cmd, "--command="+command)
+ }
+
+ if hook != "" {
+ cmd = append(cmd, "--hook="+hook)
+ }
+
+ // snap-exec is POSIXly-- options must come before positionals.
+ cmd = append(cmd, snapApp)
+ cmd = append(cmd, args...)
+
+ return syscallExec(cmd[0], cmd, snapenv.ExecEnv(info))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "os"
+ "os/user"
+ "path/filepath"
+
+ "gopkg.in/check.v1"
+
+ snaprun "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+var mockYaml = []byte(`name: snapname
+version: 1.0
+apps:
+ app:
+ command: run-app
+hooks:
+ configure:
+`)
+var mockContents = "SNAP"
+
+func (s *SnapSuite) TestInvalidParameters(c *check.C) {
+ invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "snap-name"}
+ _, err := snaprun.Parser().ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*cannot use --hook and --command together.*")
+
+ invalidParameters = []string{"run", "-r=1", "--command=command-name", "snap-name"}
+ _, err = snaprun.Parser().ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*")
+
+ invalidParameters = []string{"run", "-r=1", "snap-name"}
+ _, err = snaprun.Parser().ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*")
+
+ invalidParameters = []string{"run", "--hook=configure", "foo", "bar", "snap-name"}
+ _, err = snaprun.Parser().ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*")
+}
+
+func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.LibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+}
+
+func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ si := snaptest.MockSnap(c, string(mockYaml)+"confinement: classic\n", string(mockContents), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"), "--classic",
+ "snap.snapname.app",
+ filepath.Join(dirs.LibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+}
+
+func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ err = snaprun.SnapRunApp("snapname.app", "my-command", []string{"arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.LibExecDir, "snap-exec"),
+ "--command=my-command", "snapname.app", "arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+}
+
+func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, check.IsNil)
+ info.SideInfo.Revision = snap.R(42)
+
+ fakeHome := c.MkDir()
+ restorer := snaprun.MockUserCurrent(func() (*user.User, error) {
+ return &user.User{HomeDir: fakeHome}, nil
+ })
+ defer restorer()
+
+ err = snaprun.CreateUserDataDirs(info)
+ c.Assert(err, check.IsNil)
+ c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true)
+}
+
+func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Run a hook from the active revision
+ _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.LibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+}
+
+func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Specifically pass "unset" which would use the active version.
+ _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=unset", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.LibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+}
+
+func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ // Create both revisions 41 and 42
+ snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(41),
+ })
+ snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Run a hook on revision 41
+ _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.LibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.LibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41")
+}
+
+func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ // Only create revision 42
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ return nil
+ })
+ defer restorer()
+
+ // Attempt to run a hook on revision 41, which doesn't exist
+ _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"})
+ c.Assert(err, check.NotNil)
+ c.Check(err, check.ErrorMatches, "cannot find .*")
+}
+
+func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
+ _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "snapname"})
+ c.Assert(err, check.NotNil)
+ c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"")
+}
+
+func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ // Only create revision 42
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ called := false
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ called = true
+ return nil
+ })
+ defer restorer()
+
+ err = snaprun.SnapRunHook("snapname", "unset", "missing-hook")
+ c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`)
+ c.Check(called, check.Equals, false)
+}
+
+func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) {
+ _, err := snaprun.Parser().ParseArgs([]string{"run", "--unknown", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.ErrorMatches, "unknown flag `unknown'")
+}
+
+func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) {
+ _, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"})
+ c.Assert(err, check.ErrorMatches, "need the application to run as argument")
+}
+
+func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) {
+ _, err := snaprun.Parser().ParseArgs([]string{"run", "not-there"})
+ c.Assert(err, check.ErrorMatches, "cannot find current revision for snap not-there: readlink /snap/not-there/current: no such file or directory")
+}
+
+func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+ err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
+ c.Assert(err, check.IsNil)
+
+ // redirect exec
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // set a SNAP{,_*} variable in the environment
+ os.Setenv("SNAP_NAME", "something-else")
+ os.Setenv("SNAP_ARCH", "PDP-7")
+ defer os.Unsetenv("SNAP_NAME")
+ defer os.Unsetenv("SNAP_ARCH")
+ // but unrelated stuff is ok
+ os.Setenv("SNAP_THE_WORLD", "YES")
+ defer os.Unsetenv("SNAP_THE_WORLD")
+
+ // and ensure those SNAP_ vars get overridden
+ rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+ c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else")
+ c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7")
+ c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortSetHelp = i18n.G("Changes configuration options")
+var longSetHelp = i18n.G(`
+The set command changes the provided configuration options as requested.
+
+ $ snap set snap-name username=frank password=$PASSWORD
+
+All configuration changes are persisted at once, and only after the
+snap's configuration hook returns successfully.
+
+Nested values may be modified via a dotted path:
+
+ $ snap set author.name=frank
+`)
+
+type cmdSet struct {
+ Positional struct {
+ Snap installedSnapName
+ ConfValues []string `required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func init() {
+ addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, nil, []argDesc{
+ {
+ name: "<snap>",
+ desc: i18n.G("The snap to configure (e.g. hello-world)"),
+ }, {
+ name: i18n.G("<conf value>"),
+ desc: i18n.G("Configuration value (key=value)"),
+ },
+ })
+}
+
+func (x *cmdSet) Execute(args []string) error {
+ patchValues := make(map[string]interface{})
+ for _, patchValue := range x.Positional.ConfValues {
+ parts := strings.SplitN(patchValue, "=", 2)
+ if len(parts) != 2 {
+ return fmt.Errorf(i18n.G("invalid configuration: %q (want key=value)"), patchValue)
+ }
+ var value interface{}
+ err := json.Unmarshal([]byte(parts[1]), &value)
+ if err == nil {
+ patchValues[parts[0]] = value
+ } else {
+ // Not valid JSON-- just save the string as-is.
+ patchValues[parts[0]] = parts[1]
+ }
+ }
+
+ return configure(string(x.Positional.Snap), patchValues)
+}
+
+func configure(snapName string, patchValues map[string]interface{}) error {
+ cli := Client()
+ id, err := cli.SetConf(snapName, patchValues)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, id)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snapset "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+var validApplyYaml = []byte(`name: snapname
+version: 1.0
+hooks:
+ configure:
+`)
+var validApplyContents = ""
+
+func (s *SnapSuite) TestInvalidSetParameters(c *check.C) {
+ invalidParameters := []string{"set", "snap-name", "key", "value"}
+ _, err := snapset.Parser().ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*")
+}
+
+func (s *SnapSuite) TestSnapSetIntegrationString(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // and mock the server
+ s.mockSetConfigServer(c, "value")
+
+ // Set a config value for the active snap
+ _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=value"})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapSuite) TestSnapSetIntegrationNumber(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // and mock the server
+ s.mockSetConfigServer(c, 1.2)
+
+ // Set a config value for the active snap
+ _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=1.2"})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapSuite) TestSnapSetIntegrationJson(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // and mock the server
+ s.mockSetConfigServer(c, map[string]interface{}{"subkey": "value"})
+
+ // Set a config value for the active snap
+ _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/snaps/snapname/conf":
+ c.Check(r.Method, check.Equals, "PUT")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "key": expectedValue,
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, check.Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "syscall"
+
+ //"github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type cmdShell struct {
+ Positional struct {
+ ShellType string
+ } `positional-args:"yes"`
+}
+
+// FIXME: reenable for GA
+/*
+func init() {
+ addCommand("shell",
+ i18n.G("Run snappy shell interface"),
+ i18n.G("Run snappy shell interface"),
+ func() flags.Commander {
+ return &cmdShell{}
+ }, nil, []argDesc{{
+ name: i18n.G("<shell-type>"),
+ desc: i18n.G("The type of shell you want"),
+ }})
+}
+*/
+
+// reexec will reexec itself with sudo
+func reexecWithSudo() error {
+ args := []string{"/usr/bin/sudo"}
+ args = append(args, os.Args...)
+ env := os.Environ()
+ if err := syscall.Exec(args[0], args, env); err != nil {
+ return fmt.Errorf("failed to exec classic shell: %s", err)
+ }
+ panic("this should never be reached")
+}
+
+func (x *cmdShell) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ shellType := x.Positional.ShellType
+
+ // FIXME: make this generic so that all snaps can provide a
+ // shell
+ if shellType == "classic" {
+ if !osutil.FileExists("/snap/classic/current") {
+ return fmt.Errorf(i18n.G(`Classic dimension disabled on this system.
+Use "sudo snap install --devmode classic && sudo classic.create" to enable it.`))
+ }
+
+ // we need to re-exec if we do not run as root
+ if os.Getuid() != 0 {
+ if err := reexecWithSudo(); err != nil {
+ return err
+ }
+ }
+
+ fmt.Fprintln(Stdout, i18n.G(`Entering classic dimension`))
+ fmt.Fprintln(Stdout, i18n.G(`
+
+The home directory is shared between snappy and the classic dimension.
+Run "exit" to leave the classic shell.
+`))
+ args := []string{"/snap/bin/classic.shell"}
+ return syscall.Exec(args[0], args, os.Environ())
+ }
+
+ return fmt.Errorf(i18n.G("unsupported shell %v"), shellType)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/signtool"
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortSignHelp = i18n.G("Sign an assertion")
+var longSignHelp = i18n.G(`Sign an assertion using the specified key, using the input for headers from a JSON mapping provided through stdin, the body of the assertion can be specified through a "body" pseudo-header.
+`)
+
+type cmdSign struct {
+ KeyName keyName `short:"k" default:"default"`
+}
+
+func init() {
+ cmd := addCommand("sign", shortSignHelp, longSignHelp, func() flags.Commander {
+ return &cmdSign{}
+ }, map[string]string{"k": i18n.G("Name of the key to use, otherwise use the default key")}, nil)
+ cmd.hidden = true
+}
+
+func (x *cmdSign) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ statement, err := ioutil.ReadAll(Stdin)
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err)
+ }
+
+ keypairMgr := asserts.NewGPGKeypairManager()
+ privKey, err := keypairMgr.GetByName(string(x.KeyName))
+ if err != nil {
+ return err
+ }
+
+ signOpts := signtool.Options{
+ KeyID: privKey.PublicKey().ID(),
+ Statement: statement,
+ }
+
+ encodedAssert, err := signtool.Sign(&signOpts, keypairMgr)
+ if err != nil {
+ return err
+ }
+
+ _, err = Stdout.Write(encodedAssert)
+ if err != nil {
+ return err
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "time"
+
+ _ "golang.org/x/crypto/sha3" // expected for digests
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdSignBuild struct {
+ Positional struct {
+ Filename string
+ } `positional-args:"yes" required:"yes"`
+
+ // XXX complete DeveloperID and SnapID
+ DeveloperID string `long:"developer-id" required:"yes"`
+ SnapID string `long:"snap-id" required:"yes"`
+ KeyName keyName `short:"k" default:"default" `
+ Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable"`
+}
+
+var shortSignBuildHelp = i18n.G("Create snap build assertion")
+var longSignBuildHelp = i18n.G("Create snap-build assertion for the provided snap file.")
+
+func init() {
+ cmd := addCommand("sign-build",
+ shortSignBuildHelp,
+ longSignBuildHelp,
+ func() flags.Commander {
+ return &cmdSignBuild{}
+ }, map[string]string{
+ "developer-id": i18n.G("Identifier of the signer"),
+ "snap-id": i18n.G("Identifier of the snap package associated with the build"),
+ "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"),
+ "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"),
+ }, []argDesc{{
+ name: i18n.G("<filename>"),
+ desc: i18n.G("Filename of the snap you want to assert a build for"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdSignBuild) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ snapDigest, snapSize, err := asserts.SnapFileSHA3_384(x.Positional.Filename)
+ if err != nil {
+ return err
+ }
+
+ gkm := asserts.NewGPGKeypairManager()
+ privKey, err := gkm.GetByName(string(x.KeyName))
+ if err != nil {
+ // TRANSLATORS: %q is the key name, %v the error message
+ return fmt.Errorf(i18n.G("cannot use %q key: %v"), x.KeyName, err)
+ }
+
+ pubKey := privKey.PublicKey()
+ timestamp := time.Now().Format(time.RFC3339)
+
+ headers := map[string]interface{}{
+ "developer-id": x.DeveloperID,
+ "authority-id": x.DeveloperID,
+ "snap-sha3-384": snapDigest,
+ "snap-id": x.SnapID,
+ "snap-size": fmt.Sprintf("%d", snapSize),
+ "grade": x.Grade,
+ "timestamp": timestamp,
+ }
+
+ adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: gkm,
+ })
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot open the assertions database: %v"), err)
+ }
+
+ a, err := adb.Sign(asserts.SnapBuildType, headers, nil, pubKey.ID())
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot sign assertion: %v"), err)
+ }
+
+ _, err = Stdout.Write(asserts.Encode(a))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+type SnapSignBuildSuite struct {
+ BaseSnapSuite
+}
+
+var _ = Suite(&SnapSignBuildSuite{})
+
+func (s *SnapSignBuildSuite) TestSignBuildMandatoryFlags(c *C) {
+ _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "the required flags `--developer-id' and `--snap-id' were not specified")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSignBuildSuite) TestSignBuildMissingSnap(c *C) {
+ _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap", "--developer-id", "dev-id1", "--snap-id", "snap-id-1"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot compute snap \"foo_1_amd64.snap\" digest: open foo_1_amd64.snap: no such file or directory")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSignBuildSuite) TestSignBuildMissingKey(c *C) {
+ snapFilename := "foo_1_amd64.snap"
+ _err := ioutil.WriteFile(snapFilename, []byte("sample"), 0644)
+ c.Assert(_err, IsNil)
+ defer os.Remove(snapFilename)
+
+ tempdir := c.MkDir()
+ os.Setenv("SNAP_GNUPG_HOME", tempdir)
+ defer os.Unsetenv("SNAP_GNUPG_HOME")
+
+ _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot use \"default\" key: cannot find key named \"default\" in GPG keyring")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSignBuildSuite) TestSignBuildWorks(c *C) {
+ snapFilename := "foo_1_amd64.snap"
+ snapContent := []byte("sample")
+ _err := ioutil.WriteFile(snapFilename, snapContent, 0644)
+ c.Assert(_err, IsNil)
+ defer os.Remove(snapFilename)
+
+ tempdir := c.MkDir()
+ for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} {
+ data, err := ioutil.ReadFile(filepath.Join("test-data", fileName))
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644)
+ c.Assert(err, IsNil)
+ }
+ os.Setenv("SNAP_GNUPG_HOME", tempdir)
+ defer os.Unsetenv("SNAP_GNUPG_HOME")
+
+ _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"})
+ c.Assert(err, IsNil)
+
+ assertion, err := asserts.Decode([]byte(s.Stdout()))
+ c.Assert(err, IsNil)
+ c.Check(assertion.Type(), Equals, asserts.SnapBuildType)
+ c.Check(assertion.Revision(), Equals, 0)
+ c.Check(assertion.HeaderString("authority-id"), Equals, "dev-id1")
+ c.Check(assertion.HeaderString("developer-id"), Equals, "dev-id1")
+ c.Check(assertion.HeaderString("grade"), Equals, "stable")
+ c.Check(assertion.HeaderString("snap-id"), Equals, "snap-id-1")
+ c.Check(assertion.HeaderString("snap-size"), Equals, fmt.Sprintf("%d", len(snapContent)))
+ c.Check(assertion.HeaderString("snap-sha3-384"), Equals, "jyP7dUgb8HiRNd1SdYPp_il-YNrl6P6PgNAe-j6_7WytjKslENhMD3Of5XBU5bQK")
+
+ // check for valid signature ?!
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSignBuildSuite) TestSignBuildWorksDevelGrade(c *C) {
+ snapFilename := "foo_1_amd64.snap"
+ snapContent := []byte("sample")
+ _err := ioutil.WriteFile(snapFilename, snapContent, 0644)
+ c.Assert(_err, IsNil)
+ defer os.Remove(snapFilename)
+
+ tempdir := c.MkDir()
+ for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} {
+ data, err := ioutil.ReadFile(filepath.Join("test-data", fileName))
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644)
+ c.Assert(err, IsNil)
+ }
+ os.Setenv("SNAP_GNUPG_HOME", tempdir)
+ defer os.Unsetenv("SNAP_GNUPG_HOME")
+
+ _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1", "--grade", "devel"})
+ c.Assert(err, IsNil)
+ assertion, err := asserts.Decode([]byte(s.Stdout()))
+ c.Assert(err, IsNil)
+ c.Check(assertion.Type(), Equals, asserts.SnapBuildType)
+ c.Check(assertion.HeaderString("grade"), Equals, "devel")
+
+ // check for valid signature ?!
+ c.Check(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+var statement = []byte(fmt.Sprintf(`{"type": "snap-build",
+"authority-id": "devel1",
+"series": "16",
+"snap-id": "snapidsnapidsnapidsnapidsnapidsn",
+"snap-sha3-384": "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL",
+"snap-size": "1",
+"grade": "devel",
+"timestamp": %q
+}`, time.Now().Format(time.RFC3339)))
+
+func (s *SnapKeysSuite) TestHappyDefaultKey(c *C) {
+ s.stdin.Write(statement)
+
+ rest, err := snap.Parser().ParseArgs([]string{"sign"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ a, err := asserts.Decode(s.stdout.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SnapBuildType)
+}
+
+func (s *SnapKeysSuite) TestHappyNonDefaultKey(c *C) {
+ s.stdin.Write(statement)
+
+ rest, err := snap.Parser().ParseArgs([]string{"sign", "-k", "another"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ a, err := asserts.Decode(s.stdout.Bytes())
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SnapBuildType)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "sort"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/progress"
+)
+
+func lastLogStr(logs []string) string {
+ if len(logs) == 0 {
+ return ""
+ }
+ return logs[len(logs)-1]
+}
+
+var (
+ maxGoneTime = 5 * time.Second
+ pollTime = 100 * time.Millisecond
+)
+
+func wait(cli *client.Client, id string) (*client.Change, error) {
+ pb := progress.NewTextProgress()
+ defer func() {
+ pb.Finished()
+ }()
+
+ tMax := time.Time{}
+
+ var lastID string
+ lastLog := map[string]string{}
+ for {
+ chg, err := cli.Change(id)
+ if err != nil {
+ // a client.Error means we were able to communicate with
+ // the server (got an answer)
+ if e, ok := err.(*client.Error); ok {
+ return nil, e
+ }
+
+ // an non-client error here means the server most
+ // likely went away
+ // XXX: it actually can be a bunch of other things; fix client to expose it better
+ now := time.Now()
+ if tMax.IsZero() {
+ tMax = now.Add(maxGoneTime)
+ }
+ if now.After(tMax) {
+ return nil, err
+ }
+ pb.Spin(i18n.G("Waiting for server to restart"))
+ time.Sleep(pollTime)
+ continue
+ }
+ if !tMax.IsZero() {
+ pb.Finished()
+ tMax = time.Time{}
+ }
+
+ for _, t := range chg.Tasks {
+ switch {
+ case t.Status != "Doing":
+ continue
+ case t.Progress.Total == 1:
+ pb.Spin(t.Summary)
+ nowLog := lastLogStr(t.Log)
+ if lastLog[t.ID] != nowLog {
+ pb.Notify(nowLog)
+ lastLog[t.ID] = nowLog
+ }
+ case t.ID == lastID:
+ pb.Set(float64(t.Progress.Done))
+ default:
+ pb.Start(t.Progress.Label, float64(t.Progress.Total))
+ lastID = t.ID
+ }
+ break
+ }
+
+ if chg.Ready {
+ if chg.Status == "Done" {
+ return chg, nil
+ }
+
+ if chg.Err != "" {
+ return chg, errors.New(chg.Err)
+ }
+
+ return nil, fmt.Errorf(i18n.G("change finished in status %q with no error message"), chg.Status)
+ }
+
+ // note this very purposely is not a ticker; we want
+ // to sleep 100ms between calls, not call once every
+ // 100ms.
+ time.Sleep(pollTime)
+ }
+}
+
+var (
+ shortInstallHelp = i18n.G("Installs a snap to the system")
+ shortRemoveHelp = i18n.G("Removes a snap from the system")
+ shortRefreshHelp = i18n.G("Refreshes a snap in the system")
+ shortTryHelp = i18n.G("Tests a snap in the system")
+ shortEnableHelp = i18n.G("Enables a snap in the system")
+ shortDisableHelp = i18n.G("Disables a snap in the system")
+)
+
+var longInstallHelp = i18n.G(`
+The install command installs the named snap in the system.
+`)
+
+var longRemoveHelp = i18n.G(`
+The remove command removes the named snap from the system.
+
+By default all the snap revisions are removed, including their data and the common
+data directory. When a --revision option is passed only the specified revision is
+removed.
+`)
+
+var longRefreshHelp = i18n.G(`
+The refresh command refreshes (updates) the named snap.
+`)
+
+var longTryHelp = i18n.G(`
+The try command installs an unpacked snap into the system for testing purposes.
+The unpacked snap content continues to be used even after installation, so
+non-metadata changes there go live instantly. Metadata changes such as those
+performed in snap.yaml will require reinstallation to go live.
+
+If snap-dir argument is omitted, the try command will attempt to infer it if
+either snapcraft.yaml file and prime directory or meta/snap.yaml file can be
+found relative to current working directory.
+`)
+
+var longEnableHelp = i18n.G(`
+The enable command enables a snap that was previously disabled.
+`)
+
+var longDisableHelp = i18n.G(`
+The disable command disables a snap. The binaries and services of the
+snap will no longer be available. But all the data is still available
+and the snap can easily be enabled again.
+`)
+
+type cmdRemove struct {
+ Revision string `long:"revision"`
+ Positional struct {
+ Snaps []installedSnapName `positional-arg-name:"<snap>" required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func (x *cmdRemove) removeOne(opts *client.SnapOptions) error {
+ name := x.Positional.Snaps[0]
+
+ cli := Client()
+ changeID, err := cli.Remove(string(name), opts)
+ if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindSnapNotInstalled {
+ fmt.Fprintf(Stderr, e.Message+"\n")
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ if _, err := wait(cli, changeID); err != nil {
+ return err
+ }
+
+ fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name)
+ return nil
+}
+
+func (x *cmdRemove) removeMany(opts *client.SnapOptions) error {
+ names := make([]string, len(x.Positional.Snaps))
+ for i, s := range x.Positional.Snaps {
+ names[i] = string(s)
+ }
+
+ cli := Client()
+ changeID, err := cli.RemoveMany(names, opts)
+ if err != nil {
+ return err
+ }
+
+ chg, err := wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ var removed []string
+ if err := chg.Get("snap-names", &removed); err != nil && err != client.ErrNoData {
+ return err
+ }
+
+ seen := make(map[string]bool)
+ for _, name := range removed {
+ fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name)
+ seen[name] = true
+ }
+ for _, name := range names {
+ if !seen[name] {
+ // FIXME: this is the only reason why a name can be
+ // skipped, but it does feel awkward
+ fmt.Fprintf(Stdout, i18n.G("%s not installed\n"), name)
+ }
+ }
+
+ return nil
+
+}
+
+func (x *cmdRemove) Execute([]string) error {
+ opts := &client.SnapOptions{Revision: x.Revision}
+ if len(x.Positional.Snaps) == 1 {
+ return x.removeOne(opts)
+ }
+
+ if x.Revision != "" {
+ return errors.New(i18n.G("a single snap name is needed to specify the revision"))
+ }
+ return x.removeMany(nil)
+}
+
+type channelMixin struct {
+ Channel string `long:"channel"`
+
+ // shortcuts
+ EdgeChannel bool `long:"edge"`
+ BetaChannel bool `long:"beta"`
+ CandidateChannel bool `long:"candidate"`
+ StableChannel bool `long:"stable" `
+}
+
+type mixinDescs map[string]string
+
+func (mxd mixinDescs) also(m map[string]string) mixinDescs {
+ n := make(map[string]string, len(mxd)+len(m))
+ for k, v := range mxd {
+ n[k] = v
+ }
+ for k, v := range m {
+ n[k] = v
+ }
+ return n
+}
+
+var channelDescs = mixinDescs{
+ "channel": i18n.G("Use this channel instead of stable"),
+ "beta": i18n.G("Install from the beta channel"),
+ "edge": i18n.G("Install from the edge channel"),
+ "candidate": i18n.G("Install from the candidate channel"),
+ "stable": i18n.G("Install from the stable channel"),
+}
+
+func (mx *channelMixin) setChannelFromCommandline() error {
+ for _, ch := range []struct {
+ enabled bool
+ chName string
+ }{
+ {mx.StableChannel, "stable"},
+ {mx.CandidateChannel, "candidate"},
+ {mx.BetaChannel, "beta"},
+ {mx.EdgeChannel, "edge"},
+ } {
+ if !ch.enabled {
+ continue
+ }
+ if mx.Channel != "" {
+ return fmt.Errorf("Please specify a single channel")
+ }
+ mx.Channel = ch.chName
+ }
+
+ return nil
+}
+
+// show what has been done
+func showDone(names []string, op string) error {
+ cli := Client()
+ snaps, err := cli.List(names, nil)
+ if err != nil {
+ return err
+ }
+
+ for _, snap := range snaps {
+ channelStr := ""
+ if snap.Channel != "" && snap.Channel != "stable" {
+ channelStr = fmt.Sprintf(" (%s)", snap.Channel)
+ }
+ switch op {
+ case "install":
+ if snap.Developer != "" {
+ fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' installed\n"), snap.Name, channelStr, snap.Version, snap.Developer)
+ } else {
+ fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version)
+ }
+ case "refresh":
+ if snap.Developer != "" {
+ fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' refreshed\n"), snap.Name, channelStr, snap.Version, snap.Developer)
+ } else {
+ fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version)
+ }
+ default:
+ fmt.Fprintf(Stdout, "internal error, unknown op %q", op)
+ }
+ }
+ return nil
+}
+
+func (mx *channelMixin) asksForChannel() bool {
+ return mx.Channel != ""
+}
+
+type modeMixin struct {
+ DevMode bool `long:"devmode"`
+ JailMode bool `long:"jailmode"`
+ Classic bool `long:"classic"`
+}
+
+var modeDescs = mixinDescs{
+ "classic": i18n.G("Put snap in classic mode and disable security confinement"),
+ "devmode": i18n.G("Put snap in development mode and disable security confinement"),
+ "jailmode": i18n.G("Put snap in enforced confinement mode"),
+}
+
+var errModeConflict = errors.New(i18n.G("cannot use devmode and jailmode flags together"))
+
+func (mx modeMixin) validateMode() error {
+ if mx.DevMode && mx.JailMode {
+ return errModeConflict
+ }
+ return nil
+}
+
+func (mx modeMixin) asksForMode() bool {
+ return mx.DevMode || mx.JailMode
+}
+
+type cmdInstall struct {
+ channelMixin
+ modeMixin
+ Revision string `long:"revision"`
+
+ Dangerous bool `long:"dangerous"`
+ // alias for --dangerous, deprecated but we need to support it
+ // because we released 2.14.2 with --force-dangerous
+ ForceDangerous bool `long:"force-dangerous" hidden:"yes"`
+
+ Positional struct {
+ Snaps []remoteSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func setupAbortHandler(changeId string) {
+ // Intercept sigint
+ c := make(chan os.Signal, 2)
+ signal.Notify(c, syscall.SIGINT)
+ go func() {
+ <-c
+ cli := Client()
+ _, err := cli.Abort(changeId)
+ if err != nil {
+ fmt.Fprintf(Stderr, err.Error()+"\n")
+ }
+ }()
+}
+
+func (x *cmdInstall) installOne(name string, opts *client.SnapOptions) error {
+ var err error
+ var installFromFile bool
+ var changeID string
+
+ cli := Client()
+ if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") {
+ installFromFile = true
+ changeID, err = cli.InstallPath(name, opts)
+ } else {
+ changeID, err = cli.Install(name, opts)
+ }
+ if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindSnapAlreadyInstalled {
+ fmt.Fprintf(Stderr, e.Message+"\n")
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ setupAbortHandler(changeID)
+
+ chg, err := wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ // extract the snapName from the change, important for sideloaded
+ var snapName string
+
+ if installFromFile {
+ if err := chg.Get("snap-name", &snapName); err != nil {
+ return fmt.Errorf("cannot extract the snap-name from local file %q: %s", name, err)
+ }
+ name = snapName
+ }
+
+ return showDone([]string{name}, "install")
+}
+
+func (x *cmdInstall) installMany(names []string, opts *client.SnapOptions) error {
+ // sanity check
+ for _, name := range names {
+ if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") {
+ return fmt.Errorf("only one snap file can be installed at a time")
+ }
+ }
+
+ cli := Client()
+ changeID, err := cli.InstallMany(names, opts)
+ if err != nil {
+ return err
+ }
+
+ setupAbortHandler(changeID)
+
+ chg, err := wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ var installed []string
+ if err := chg.Get("snap-names", &installed); err != nil && err != client.ErrNoData {
+ return err
+ }
+
+ if len(installed) > 0 {
+ if err := showDone(installed, "install"); err != nil {
+ return err
+ }
+ }
+
+ // show skipped
+ seen := make(map[string]bool)
+ for _, name := range installed {
+ seen[name] = true
+ }
+ for _, name := range names {
+ if !seen[name] {
+ // FIXME: this is the only reason why a name can be
+ // skipped, but it does feel awkward
+ fmt.Fprintf(Stdout, i18n.G("%s already installed\n"), name)
+ }
+ }
+
+ return nil
+}
+
+func (x *cmdInstall) Execute([]string) error {
+ if err := x.setChannelFromCommandline(); err != nil {
+ return err
+ }
+ if err := x.validateMode(); err != nil {
+ return err
+ }
+
+ dangerous := x.Dangerous || x.ForceDangerous
+ opts := &client.SnapOptions{
+ Channel: x.Channel,
+ DevMode: x.DevMode,
+ JailMode: x.JailMode,
+ Classic: x.Classic,
+ Revision: x.Revision,
+ Dangerous: dangerous,
+ }
+
+ names := make([]string, len(x.Positional.Snaps))
+ for i, name := range x.Positional.Snaps {
+ names[i] = string(name)
+ }
+
+ if len(names) == 1 {
+ return x.installOne(names[0], opts)
+ }
+
+ if x.asksForMode() || x.asksForChannel() {
+ return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags"))
+ }
+
+ return x.installMany(names, nil)
+}
+
+type cmdRefresh struct {
+ channelMixin
+ modeMixin
+
+ Revision string `long:"revision"`
+ List bool `long:"list"`
+ IgnoreValidation bool `long:"ignore-validation"`
+ Positional struct {
+ Snaps []installedSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"yes"`
+}
+
+func refreshMany(snaps []string, opts *client.SnapOptions) error {
+ cli := Client()
+ changeID, err := cli.RefreshMany(snaps, opts)
+ if err != nil {
+ return err
+ }
+
+ chg, err := wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ var refreshed []string
+ if err := chg.Get("snap-names", &refreshed); err != nil && err != client.ErrNoData {
+ return err
+ }
+
+ if len(refreshed) > 0 {
+ return showDone(refreshed, "refresh")
+ }
+
+ fmt.Fprintln(Stderr, i18n.G("All snaps up to date."))
+
+ return nil
+}
+
+func refreshOne(name string, opts *client.SnapOptions) error {
+ cli := Client()
+ changeID, err := cli.Refresh(name, opts)
+ if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNoUpdateAvailable {
+ fmt.Fprintf(Stderr, e.Message+"\n")
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ if _, err := wait(cli, changeID); err != nil {
+ return err
+ }
+
+ return showDone([]string{name}, "refresh")
+}
+
+func listRefresh() error {
+ cli := Client()
+ snaps, _, err := cli.Find(&client.FindOptions{
+ Refresh: true,
+ })
+ if err != nil {
+ return err
+ }
+ if len(snaps) == 0 {
+ fmt.Fprintln(Stderr, i18n.G("All snaps up to date."))
+ return nil
+ }
+
+ sort.Sort(snapsByName(snaps))
+
+ w := tabWriter()
+ defer w.Flush()
+
+ fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes"))
+ for _, snap := range snaps {
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromRemote(snap, nil))
+ }
+
+ return nil
+}
+
+func (x *cmdRefresh) Execute([]string) error {
+ if err := x.setChannelFromCommandline(); err != nil {
+ return err
+ }
+ if err := x.validateMode(); err != nil {
+ return err
+ }
+
+ if x.List {
+ if x.asksForMode() || x.asksForChannel() {
+ return errors.New(i18n.G("--list does not take mode nor channel flags"))
+ }
+
+ return listRefresh()
+ }
+ names := make([]string, len(x.Positional.Snaps))
+ for i, name := range x.Positional.Snaps {
+ names[i] = string(name)
+ }
+ if len(x.Positional.Snaps) == 1 {
+ opts := &client.SnapOptions{
+ Channel: x.Channel,
+ DevMode: x.DevMode,
+ Classic: x.Classic,
+ JailMode: x.JailMode,
+ IgnoreValidation: x.IgnoreValidation,
+ Revision: x.Revision,
+ }
+ return refreshOne(names[0], opts)
+ }
+
+ if x.asksForMode() || x.asksForChannel() {
+ return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags"))
+ }
+
+ if x.IgnoreValidation {
+ return errors.New(i18n.G("a single snap name must be specified when ignoring validation"))
+ }
+
+ return refreshMany(names, nil)
+}
+
+type cmdTry struct {
+ modeMixin
+ Positional struct {
+ SnapDir string `positional-arg-name:"<snap-dir>"`
+ } `positional-args:"yes"`
+}
+
+func (x *cmdTry) Execute([]string) error {
+ if err := x.validateMode(); err != nil {
+ return err
+ }
+ cli := Client()
+ name := x.Positional.SnapDir
+ opts := &client.SnapOptions{
+ DevMode: x.DevMode,
+ JailMode: x.JailMode,
+ }
+
+ if name == "" {
+ if osutil.FileExists("snapcraft.yaml") && osutil.IsDirectory("prime") {
+ name = "prime"
+ } else {
+ if osutil.FileExists("meta/snap.yaml") {
+ name = "./"
+ }
+ }
+ if name == "" {
+ return fmt.Errorf(i18n.G("error: the `<snap-dir>` argument was not provided and couldn't be inferred"))
+ }
+ }
+
+ path, err := filepath.Abs(name)
+ if err != nil {
+ // TRANSLATORS: %q gets what the user entered, %v gets the resulting error message
+ return fmt.Errorf(i18n.G("cannot get full path for %q: %v"), name, err)
+ }
+
+ changeID, err := cli.Try(path, opts)
+ if err != nil {
+ return err
+ }
+
+ chg, err := wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ // extract the snap name
+ var snapName string
+ if err := chg.Get("snap-name", &snapName); err != nil {
+ // TRANSLATORS: %q gets the snap name, %v gets the resulting error message
+ return fmt.Errorf(i18n.G("cannot extract the snap-name from local file %q: %v"), name, err)
+ }
+ name = snapName
+
+ // show output as speced
+ snaps, err := cli.List([]string{name}, nil)
+ if err != nil {
+ return err
+ }
+ if len(snaps) != 1 {
+ // TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it
+ return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps)
+ }
+ snap := snaps[0]
+ // TRANSLATORS: 1. snap name, 2. snap version (keep those together please). the 3rd %s is a path (where it's mounted from).
+ fmt.Fprintf(Stdout, i18n.G("%s %s mounted from %s\n"), name, snap.Version, path)
+ return nil
+}
+
+type cmdEnable struct {
+ Positional struct {
+ Snap installedSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func (x *cmdEnable) Execute([]string) error {
+ cli := Client()
+ name := string(x.Positional.Snap)
+ opts := &client.SnapOptions{}
+ changeID, err := cli.Enable(name, opts)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(Stdout, i18n.G("%s enabled\n"), name)
+ return nil
+}
+
+type cmdDisable struct {
+ Positional struct {
+ Snap installedSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func (x *cmdDisable) Execute([]string) error {
+ cli := Client()
+ name := string(x.Positional.Snap)
+ opts := &client.SnapOptions{}
+ changeID, err := cli.Disable(name, opts)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, changeID)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(Stdout, i18n.G("%s disabled\n"), name)
+ return nil
+}
+
+type cmdRevert struct {
+ modeMixin
+ Revision string `long:"revision"`
+ Positional struct {
+ Snap installedSnapName `positional-arg-name:"<snap>"`
+ } `positional-args:"yes"`
+}
+
+var shortRevertHelp = i18n.G("Reverts the given snap to the previous state")
+var longRevertHelp = i18n.G(`
+The revert command reverts the given snap to its state before
+the latest refresh. This will reactivate the previous snap revision,
+and will use the original data that was associated with that revision,
+discarding any data changes that were done by the latest revision. As
+an exception, data which the snap explicitly chooses to share across
+revisions is not touched by the revert process.
+`)
+
+func (x *cmdRevert) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ if err := x.validateMode(); err != nil {
+ return err
+ }
+
+ cli := Client()
+ name := string(x.Positional.Snap)
+ opts := &client.SnapOptions{DevMode: x.DevMode, JailMode: x.JailMode, Revision: x.Revision}
+ changeID, err := cli.Revert(name, opts)
+ if err != nil {
+ return err
+ }
+
+ if _, err := wait(cli, changeID); err != nil {
+ return err
+ }
+
+ // show output as speced
+ snaps, err := cli.List([]string{name}, nil)
+ if err != nil {
+ return err
+ }
+ if len(snaps) != 1 {
+ // TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it
+ return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps)
+ }
+ snap := snaps[0]
+ fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), name, snap.Version)
+ return nil
+}
+
+func init() {
+ addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} },
+ map[string]string{"revision": i18n.G("Remove only the given revision")}, nil)
+ addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} },
+ channelDescs.also(modeDescs).also(map[string]string{
+ "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"),
+ "dangerous": i18n.G("Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"),
+ "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"),
+ }), nil)
+ addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} },
+ channelDescs.also(modeDescs).also(map[string]string{
+ "revision": i18n.G("Refresh to the given revision"),
+ "list": i18n.G("Show available snaps for refresh"),
+ "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"),
+ }), nil)
+ addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, modeDescs, nil)
+ addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, nil, nil)
+ addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, nil, nil)
+ addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, modeDescs.also(map[string]string{
+ "revision": "Revert to the given revision",
+ }), nil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "time"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+type snapOpTestServer struct {
+ c *check.C
+
+ checker func(r *http.Request)
+ n int
+ total int
+ channel string
+}
+
+var _ = check.Suite(&SnapOpSuite{})
+
+func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) {
+ switch t.n {
+ case 0:
+ t.checker(r)
+ t.c.Check(r.Method, check.Equals, "POST")
+ w.WriteHeader(http.StatusAccepted)
+ fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`)
+ case 1:
+ t.c.Check(r.Method, check.Equals, "GET")
+ t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
+ case 2:
+ t.c.Check(r.Method, check.Equals, "GET")
+ t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "foo"}}}`)
+ case 3:
+ t.c.Check(r.Method, check.Equals, "GET")
+ t.c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"%s"}]}\n`, t.channel)
+ default:
+ t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1)
+ }
+
+ t.n++
+}
+
+type SnapOpSuite struct {
+ BaseSnapSuite
+
+ restoreAll func()
+ srv snapOpTestServer
+}
+
+func (s *SnapOpSuite) SetUpTest(c *check.C) {
+ s.BaseSnapSuite.SetUpTest(c)
+
+ restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond)
+ restorePollTime := snap.MockPollTime(time.Millisecond)
+ s.restoreAll = func() {
+ restoreClientRetry()
+ restorePollTime()
+ }
+
+ s.srv = snapOpTestServer{
+ c: c,
+ total: 4,
+ }
+}
+
+func (s *SnapOpSuite) TearDownTest(c *check.C) {
+ s.restoreAll()
+ s.BaseSnapSuite.TearDownTest(c)
+}
+
+func (s *SnapOpSuite) TestWait(c *check.C) {
+ restore := snap.MockMaxGoneTime(time.Millisecond)
+ defer restore()
+
+ // lazy way of getting a URL that won't work nor break stuff
+ server := httptest.NewServer(nil)
+ snap.ClientConfig.BaseURL = server.URL
+ server.Close()
+
+ d := c.MkDir()
+ oldStdout := os.Stdout
+ stdout, err := ioutil.TempFile(d, "stdout")
+ c.Assert(err, check.IsNil)
+ defer func() {
+ os.Stdout = oldStdout
+ stdout.Close()
+ os.Remove(stdout.Name())
+ }()
+ os.Stdout = stdout
+
+ cli := snap.Client()
+ chg, err := snap.Wait(cli, "x")
+ c.Assert(chg, check.IsNil)
+ c.Assert(err, check.NotNil)
+ buf, err := ioutil.ReadFile(stdout.Name())
+ c.Assert(err, check.IsNil)
+ c.Check(string(buf), check.Matches, "(?ms).*Waiting for server to restart.*")
+}
+
+func (s *SnapOpSuite) TestWaitRecovers(c *check.C) {
+ restore := snap.MockMaxGoneTime(time.Millisecond)
+ defer restore()
+
+ nah := true
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ if nah {
+ nah = false
+ return
+ }
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`)
+ })
+
+ d := c.MkDir()
+ oldStdout := os.Stdout
+ stdout, err := ioutil.TempFile(d, "stdout")
+ c.Assert(err, check.IsNil)
+ defer func() {
+ os.Stdout = oldStdout
+ stdout.Close()
+ os.Remove(stdout.Name())
+ }()
+ os.Stdout = stdout
+
+ cli := snap.Client()
+ chg, err := snap.Wait(cli, "x")
+ // we got the change
+ c.Assert(chg, check.NotNil)
+ c.Assert(err, check.IsNil)
+ buf, err := ioutil.ReadFile(stdout.Name())
+ c.Assert(err, check.IsNil)
+
+ // but only after recovering
+ c.Check(string(buf), check.Matches, "(?ms).*Waiting for server to restart.*")
+}
+
+func (s *SnapOpSuite) TestInstall(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "channel": "chan",
+ })
+ s.srv.channel = "chan"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "chan", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(chan\) 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallDevMode(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "devmode": true,
+ "channel": "chan",
+ })
+ s.srv.channel = "chan"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "chan", "--devmode", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(chan\) 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallClassic(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "classic": true,
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallPath(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, check.IsNil)
+ c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"devmode\"\r\n\r\nfalse\r\n.*")
+ }
+
+ snapBody := []byte("snap-data")
+ s.RedirectClientToTestServer(s.srv.handle)
+ snapPath := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snapPath, snapBody, 0644)
+ c.Assert(err, check.IsNil)
+
+ rest, err := snap.Parser().ParseArgs([]string{"install", snapPath})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallPathDevMode(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, check.IsNil)
+ c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"devmode\"\r\n\r\ntrue\r\n.*")
+ }
+
+ snapBody := []byte("snap-data")
+ s.RedirectClientToTestServer(s.srv.handle)
+ snapPath := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snapPath, snapBody, 0644)
+ c.Assert(err, check.IsNil)
+
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--devmode", snapPath})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallPathClassic(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, check.IsNil)
+ c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"classic\"\r\n\r\ntrue\r\n.*")
+ }
+
+ snapBody := []byte("snap-data")
+ s.RedirectClientToTestServer(s.srv.handle)
+ snapPath := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snapPath, snapBody, 0644)
+ c.Assert(err, check.IsNil)
+
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", snapPath})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestInstallPathDangerous(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, check.IsNil)
+ c.Assert(string(postData), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*")
+ }
+
+ snapBody := []byte("snap-data")
+ s.RedirectClientToTestServer(s.srv.handle)
+ snapPath := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snapPath, snapBody, 0644)
+ c.Assert(err, check.IsNil)
+
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--dangerous", snapPath})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestRevert(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "revert",
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"revert", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo reverted to 1.0`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+func (s *SnapSuite) TestRefreshList(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ c.Check(r.URL.Query().Get("select"), check.Equals, "refresh")
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2update1", "developer": "bar", "revision":17,"summary":"some summary"}]}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser().ParseArgs([]string{"refresh", "--list"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes
+foo +4.2update1 +17 +bar +-.*
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestRefreshListErr(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--list", "--beta"})
+ c.Check(err, check.ErrorMatches, "--list does not take .* flags")
+}
+
+func (s *SnapOpSuite) TestRefreshOne(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ })
+ }
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' refreshed`)
+
+}
+
+func (s *SnapOpSuite) TestRefreshOneSwitchChannel(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "channel": "beta",
+ })
+ s.srv.channel = "beta"
+ }
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from 'bar' refreshed`)
+}
+
+func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/one")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "classic": true,
+ })
+ }
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--classic", "one"})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapOpSuite) TestRefreshOneDevmode(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/one")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "devmode": true,
+ })
+ }
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode", "one"})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapOpSuite) TestRefreshOneJailmode(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/one")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "jailmode": true,
+ })
+ }
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "one"})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapOpSuite) TestRefreshOneIgnoreValidation(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/one")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "ignore-validation": true,
+ })
+ }
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one"})
+ c.Assert(err, check.IsNil)
+}
+
+func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"})
+ c.Assert(err, check.ErrorMatches, `cannot use devmode and jailmode flags together`)
+}
+
+func (s *SnapOpSuite) TestRefreshOneChanErr(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "--channel=foo", "one"})
+ c.Assert(err, check.ErrorMatches, `Please specify a single channel`)
+}
+
+func (s *SnapOpSuite) TestRefreshAllChannel(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta"})
+ c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`)
+}
+
+func (s *SnapOpSuite) TestRefreshManyChannel(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "one", "two"})
+ c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`)
+}
+
+func (s *SnapOpSuite) TestRefreshManyIgnoreValidation(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one", "two"})
+ c.Assert(err, check.ErrorMatches, `a single snap name must be specified when ignoring validation`)
+}
+
+func (s *SnapOpSuite) TestRefreshAllModeFlags(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode"})
+ c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`)
+}
+
+func (s *SnapOpSuite) runTryTest(c *check.C, devmode bool) {
+ // pass relative path to cmd
+ tryDir := "some-dir"
+
+ s.srv.checker = func(r *http.Request) {
+ // ensure the client always sends the absolute path
+ fullTryDir, err := filepath.Abs(tryDir)
+ c.Assert(err, check.IsNil)
+
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ postData, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, check.IsNil)
+ c.Assert(string(postData), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ntry\r\n.*")
+ c.Assert(string(postData), check.Matches, fmt.Sprintf("(?s).*Content-Disposition: form-data; name=\"snap-path\"\r\n\r\n%s\r\n.*", regexp.QuoteMeta(fullTryDir)))
+ c.Assert(string(postData), check.Matches, fmt.Sprintf("(?s).*Content-Disposition: form-data; name=\"devmode\"\r\n\r\n%s\r\n.*", strconv.FormatBool(devmode)))
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+
+ cmd := []string{"try", tryDir}
+ if devmode {
+ cmd = append(cmd, "--devmode")
+ }
+
+ rest, err := snap.Parser().ParseArgs(cmd)
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm).*foo 1.0 mounted from .*%s`, tryDir))
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestTryNoDevMode(c *check.C) {
+ s.runTryTest(c, false)
+}
+func (s *SnapOpSuite) TestTryDevMode(c *check.C) {
+ s.runTryTest(c, true)
+}
+
+func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) {
+ _, err := snap.Parser().ParseArgs([]string{"install", "--edge", "--beta", "some-snap"})
+ c.Assert(err, check.ErrorMatches, "Please specify a single channel")
+}
+
+func (s *SnapSuite) TestRefreshChannelDuplicationError(c *check.C) {
+ _, err := snap.Parser().ParseArgs([]string{"refresh", "--edge", "--beta", "some-snap"})
+ c.Assert(err, check.ErrorMatches, "Please specify a single channel")
+}
+
+func (s *SnapOpSuite) TestInstallFromChannel(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "channel": "edge",
+ })
+ s.srv.channel = "edge"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"install", "--edge", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(edge\) 1.0 from 'bar' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestEnable(c *check.C) {
+ s.srv.total = 3
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "enable",
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"enable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo enabled`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestDisable(c *check.C) {
+ s.srv.total = 3
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "disable",
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"disable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo disabled`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestRemove(c *check.C) {
+ s.srv.total = 3
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "remove",
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser().ParseArgs([]string{"remove", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
+func (s *SnapOpSuite) TestRemoveManyRevision(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"remove", "--revision=17", "one", "two"})
+ c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify the revision`)
+}
+
+func (s *SnapOpSuite) TestRemoveMany(c *check.C) {
+ total := 3
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "remove",
+ "snaps": []interface{}{"one", "two"},
+ })
+
+ c.Check(r.Method, check.Equals, "POST")
+ w.WriteHeader(http.StatusAccepted)
+ fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
+ case 2:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`)
+ default:
+ c.Fatalf("expected to get %d requests, now on %d", total, n+1)
+ }
+
+ n++
+ })
+
+ rest, err := snap.Parser().ParseArgs([]string{"remove", "one", "two"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*one removed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*two removed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(n, check.Equals, total)
+}
+
+func (s *SnapOpSuite) TestInstallManyChannel(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"install", "--beta", "one", "two"})
+ c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`)
+}
+
+func (s *SnapOpSuite) TestInstallManyMixFileAndStore(c *check.C) {
+ s.RedirectClientToTestServer(nil)
+ _, err := snap.Parser().ParseArgs([]string{"install", "store-snap", "./local.snap"})
+ c.Assert(err, check.ErrorMatches, `only one snap file can be installed at a time`)
+}
+
+func (s *SnapOpSuite) TestInstallMany(c *check.C) {
+ total := 4
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "snaps": []interface{}{"one", "two"},
+ })
+
+ c.Check(r.Method, check.Equals, "POST")
+ w.WriteHeader(http.StatusAccepted)
+ fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
+ case 2:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`)
+ case 3:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "one", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "revision":42, "channel":"edge"}]}\n`)
+
+ default:
+ c.Fatalf("expected to get %d requests, now on %d", total, n+1)
+ }
+
+ n++
+ })
+
+ rest, err := snap.Parser().ParseArgs([]string{"install", "one", "two"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ // note that (stable) is omitted
+ c.Check(s.Stdout(), check.Matches, `(?sm).*one 1.0 from 'bar' installed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*two \(edge\) 2.0 from 'baz' installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(n, check.Equals, total)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdUnalias struct {
+ Positionals struct {
+ Snap installedSnapName `required:"yes"`
+ Aliases []string `required:"yes"`
+ } `positional-args:"true"`
+}
+
+var shortUnaliasHelp = i18n.G("Disables the given aliases")
+var longUnaliasHelp = i18n.G(`
+The unalias command disables explicitly the given application aliases defined by the snap.
+`)
+
+func init() {
+ addCommand("unalias", shortUnaliasHelp, longUnaliasHelp, func() flags.Commander {
+ return &cmdUnalias{}
+ }, nil, []argDesc{
+ {name: "<snap>"},
+ {name: i18n.G("<alias>")},
+ })
+}
+
+func (x *cmdUnalias) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ snapName := string(x.Positionals.Snap)
+ aliases := x.Positionals.Aliases
+
+ cli := Client()
+ id, err := cli.Unalias(snapName, aliases)
+ if err != nil {
+ return err
+ }
+
+ _, err = wait(cli, id)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestUnaliasHelp(c *C) {
+ msg := `Usage:
+ snap.test [OPTIONS] unalias [<snap>] [<alias>...]
+
+The unalias command disables explicitly the given application aliases defined
+by the snap.
+
+Application Options:
+ --version Print the version and exit
+
+Help Options:
+ -h, --help Show this help message
+`
+ rest, err := Parser().ParseArgs([]string{"unalias", "--help"})
+ c.Assert(err.Error(), Equals, msg)
+ c.Assert(rest, DeepEquals, []string{})
+}
+
+func (s *SnapSuite) TestUnalias(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/aliases":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "unalias",
+ "snap": "alias-snap",
+ "aliases": []interface{}{"alias1", "alias2"},
+ })
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser().ParseArgs([]string{"unalias", "alias-snap", "alias1", "alias2"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortVersionHelp = i18n.G("Shows version details")
+var longVersionHelp = i18n.G(`
+The version command displays the versions of the running client, server,
+and operating system.
+`)
+
+type cmdVersion struct{}
+
+func init() {
+ addCommand("version", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil)
+}
+
+func (cmd cmdVersion) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ printVersions()
+ return nil
+}
+
+func printVersions() error {
+ sv, err := Client().ServerVersion()
+ if err != nil {
+ sv = &client.ServerVersion{
+ Version: i18n.G("unavailable"),
+ Series: "-",
+ OSID: "-",
+ OSVersionID: "-",
+ }
+ }
+
+ w := tabWriter()
+
+ fmt.Fprintf(w, "snap\t%s\n", cmd.Version)
+ fmt.Fprintf(w, "snapd\t%s\n", sv.Version)
+ fmt.Fprintf(w, "series\t%s\n", sv.Series)
+ if sv.OnClassic {
+ fmt.Fprintf(w, "%s\t%s\n", sv.OSID, sv.OSVersionID)
+ }
+ w.Flush()
+
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestVersionCommandOnClassic(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`)
+ })
+ restore := mockArgs("snap", "version")
+ defer restore()
+ restore = mockVersion("4.56")
+ defer restore()
+
+ _, err := snap.Parser().ParseArgs([]string{"version"})
+ c.Assert(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestVersionCommandOnAllSnap(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`)
+ })
+ restore := mockArgs("snap", "--version")
+ defer restore()
+ restore = mockVersion("4.56")
+ defer restore()
+
+ _, err := snap.Parser().ParseArgs([]string{"version"})
+ c.Assert(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n")
+ c.Assert(s.Stderr(), Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdWatch struct {
+ Positional struct {
+ ChangeID changeID `positional-arg-name:"<change-id>"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+var shortWatchHelp = i18n.G("Watch a change in progress")
+var longWatchHelp = i18n.G(`
+The watch command waits for the given change-id to finish and shows progress
+(if available).
+`)
+
+func init() {
+ addCommand("watch", shortWatchHelp, longWatchHelp, func() flags.Commander {
+ return &cmdWatch{}
+ }, nil, []argDesc{{
+ name: i18n.G("<change-id>"),
+ desc: i18n.G("Change ID"),
+ }})
+}
+
+func (x *cmdWatch) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ cli := Client()
+ _, err := wait(cli, string(x.Positional.ChangeID))
+
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+var fmtWatchChangeJSON = `{"type": "sync", "result": {
+ "id": "42",
+ "kind": "some-kind",
+ "summary": "some summary...",
+ "status": "Doing",
+ "ready": false,
+ "tasks": [{"id": "84", "kind": "bar", "summary": "some summary", "status": "Doing", "progress": {"label": "my-snap", "done": %d, "total": %d}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}]
+}}`
+
+func (s *SnapSuite) TestCmdWatch(c *C) {
+ restore := snap.MockMaxGoneTime(time.Millisecond)
+ defer restore()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/changes/42")
+ fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024)
+ case 1:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/changes/42")
+ fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024)
+ case 2:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"id": "42", "ready": true, "status": "Done"}}`)
+ }
+ n++
+ })
+
+ oldStdout := os.Stdout
+ stdout, err := ioutil.TempFile("", "stdout")
+ c.Assert(err, IsNil)
+ defer func() {
+ os.Stdout = oldStdout
+ stdout.Close()
+ os.Remove(stdout.Name())
+ }()
+ os.Stdout = stdout
+
+ _, err = snap.Parser().ParseArgs([]string{"watch", "42"})
+ os.Stdout = oldStdout
+ c.Assert(err, IsNil)
+ c.Check(n, Equals, 3)
+
+ buf, err := ioutil.ReadFile(stdout.Name())
+ c.Assert(err, IsNil)
+ c.Check(string(buf), testutil.Contains, "\rmy-snap 50.00 KB / 100.00 KB")
+}
--- /dev/null
+package main
+
+import (
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/client"
+)
+
+type installedSnapName string
+
+func (s installedSnapName) Complete(match string) []flags.Completion {
+ cli := Client()
+ snaps, err := cli.List(nil, nil)
+ if err != nil {
+ return nil
+ }
+
+ ret := make([]flags.Completion, 0, len(snaps))
+ for _, snap := range snaps {
+ if strings.HasPrefix(snap.Name, match) {
+ ret = append(ret, flags.Completion{Item: snap.Name})
+ }
+ }
+
+ return ret
+}
+
+type remoteSnapName string
+
+func (s remoteSnapName) Complete(match string) []flags.Completion {
+ if len(match) < 3 {
+ return nil
+ }
+ cli := Client()
+ snaps, _, err := cli.Find(&client.FindOptions{
+ Prefix: true,
+ Query: match,
+ })
+ if err != nil {
+ return nil
+ }
+ ret := make([]flags.Completion, len(snaps))
+ for i, snap := range snaps {
+ ret[i] = flags.Completion{Item: snap.Name}
+ }
+ return ret
+}
+
+type anySnapName string
+
+func (s anySnapName) Complete(match string) []flags.Completion {
+ res := installedSnapName(s).Complete(match)
+ seen := make(map[string]bool)
+ for _, x := range res {
+ seen[x.Item] = true
+ }
+
+ for _, x := range remoteSnapName(s).Complete(match) {
+ if !seen[x.Item] {
+ res = append(res, x)
+ }
+ }
+
+ return res
+}
+
+type changeID string
+
+func (s changeID) Complete(match string) []flags.Completion {
+ cli := Client()
+ changes, err := cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll})
+ if err != nil {
+ return nil
+ }
+
+ ret := make([]flags.Completion, 0, len(changes))
+ for _, change := range changes {
+ if strings.HasPrefix(change.ID, match) {
+ ret = append(ret, flags.Completion{Item: change.ID})
+ }
+ }
+
+ return ret
+}
+
+type keyName string
+
+func (s keyName) Complete(match string) []flags.Completion {
+ var res []flags.Completion
+ asserts.NewGPGKeypairManager().Walk(func(_ asserts.PrivateKey, _ string, uid string) error {
+ if strings.HasPrefix(uid, match) {
+ res = append(res, flags.Completion{Item: uid})
+ }
+ return nil
+ })
+ return res
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "os/user"
+ "time"
+
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/store"
+)
+
+var RunMain = run
+
+var (
+ CreateUserDataDirs = createUserDataDirs
+ SnapRunApp = snapRunApp
+ SnapRunHook = snapRunHook
+ Wait = wait
+ ResolveApp = resolveApp
+)
+
+func MockPollTime(d time.Duration) (restore func()) {
+ d0 := pollTime
+ pollTime = d
+ return func() {
+ pollTime = d0
+ }
+}
+
+func MockMaxGoneTime(d time.Duration) (restore func()) {
+ d0 := maxGoneTime
+ maxGoneTime = d
+ return func() {
+ maxGoneTime = d0
+ }
+}
+
+func MockSyscallExec(f func(string, []string, []string) error) (restore func()) {
+ syscallExecOrig := syscallExec
+ syscallExec = f
+ return func() {
+ syscallExec = syscallExecOrig
+ }
+}
+
+func MockUserCurrent(f func() (*user.User, error)) (restore func()) {
+ userCurrentOrig := userCurrent
+ userCurrent = f
+ return func() {
+ userCurrent = userCurrentOrig
+ }
+}
+
+func MockStoreNew(f func(*store.Config, auth.AuthContext) *store.Store) (restore func()) {
+ storeNewOrig := storeNew
+ storeNew = f
+ return func() {
+ storeNew = storeNewOrig
+ }
+}
+
+func MockMountInfoPath(newMountInfoPath string) (restore func()) {
+ mountInfoPathOrig := mountInfoPath
+ mountInfoPath = newMountInfoPath
+ return func() {
+ mountInfoPath = mountInfoPathOrig
+ }
+}
+
+var AutoImportCandidates = autoImportCandidates
+
+func AliasInfoLess(snapName1, alias1, app1, snapName2, alias2, app2 string) bool {
+ x := aliasInfos{
+ &aliasInfo{
+ Snap: snapName1,
+ Alias: alias1,
+ App: app1,
+ },
+ &aliasInfo{
+ Snap: snapName2,
+ Alias: alias2,
+ App: app2,
+ },
+ }
+ return x.Less(0, 1)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+)
+
+// FIXME: drop once gpg2 is the default
+var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg2"})
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+// AttributePair contains a pair of key-value strings
+type AttributePair struct {
+ // The key
+ Key string
+ // The value
+ Value string
+}
+
+// UnmarshalFlag parses a string into an AttributePair
+func (ap *AttributePair) UnmarshalFlag(value string) error {
+ parts := strings.SplitN(value, "=", 2)
+ if len(parts) < 2 || parts[0] == "" {
+ ap.Key = ""
+ ap.Value = ""
+ return fmt.Errorf(i18n.G("invalid attribute: %q (want key=value)"), value)
+ }
+ ap.Key = parts[0]
+ ap.Value = parts[1]
+ return nil
+}
+
+// AttributePairSliceToMap converts a slice of AttributePair into a map
+func AttributePairSliceToMap(attrs []AttributePair) map[string]string {
+ result := make(map[string]string)
+ for _, attr := range attrs {
+ result[attr.Key] = attr.Value
+ }
+ return result
+}
+
+// SnapAndName holds a snap name and a plug or slot name.
+type SnapAndName struct {
+ Snap string
+ Name string
+}
+
+// UnmarshalFlag unmarshals snap and plug or slot name.
+func (sn *SnapAndName) UnmarshalFlag(value string) error {
+ parts := strings.Split(value, ":")
+ sn.Snap = ""
+ sn.Name = ""
+ switch len(parts) {
+ case 1:
+ sn.Snap = parts[0]
+ case 2:
+ sn.Snap = parts[0]
+ sn.Name = parts[1]
+ // Reject "snap:" (that should be spelled as "snap")
+ if sn.Name == "" {
+ sn.Snap = ""
+ }
+ }
+ if sn.Snap == "" && sn.Name == "" {
+ return fmt.Errorf(i18n.G("invalid value: %q (want snap:name or snap)"), value)
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+type AttributePairSuite struct{}
+
+var _ = Suite(&AttributePairSuite{})
+
+func (s *AttributePairSuite) TestUnmarshalFlagAttributePair(c *C) {
+ var ap AttributePair
+ // Typical
+ err := ap.UnmarshalFlag("key=value")
+ c.Assert(err, IsNil)
+ c.Check(ap.Key, Equals, "key")
+ c.Check(ap.Value, Equals, "value")
+ // Empty key
+ err = ap.UnmarshalFlag("=value")
+ c.Assert(err, ErrorMatches, `invalid attribute: "=value" \(want key=value\)`)
+ c.Check(ap.Key, Equals, "")
+ c.Check(ap.Value, Equals, "")
+ // Empty value
+ err = ap.UnmarshalFlag("key=")
+ c.Assert(err, IsNil)
+ c.Check(ap.Key, Equals, "key")
+ c.Check(ap.Value, Equals, "")
+ // Both key and value empty
+ err = ap.UnmarshalFlag("=")
+ c.Assert(err, ErrorMatches, `invalid attribute: "=" \(want key=value\)`)
+ c.Check(ap.Key, Equals, "")
+ c.Check(ap.Value, Equals, "")
+ // Value containing =
+ err = ap.UnmarshalFlag("key=value=more")
+ c.Assert(err, IsNil)
+ c.Check(ap.Key, Equals, "key")
+ c.Check(ap.Value, Equals, "value=more")
+ // Malformed format
+ err = ap.UnmarshalFlag("malformed")
+ c.Assert(err, ErrorMatches, `invalid attribute: "malformed" \(want key=value\)`)
+ c.Check(ap.Key, Equals, "")
+ c.Check(ap.Value, Equals, "")
+}
+
+func (s *AttributePairSuite) TestAttributePairSliceToMap(c *C) {
+ attrs := []AttributePair{
+ {"key1", "value1"},
+ {"key2", "value2"},
+ }
+ m := AttributePairSliceToMap(attrs)
+ c.Check(m, DeepEquals, map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ })
+}
+
+type SnapAndNameSuite struct{}
+
+var _ = Suite(&SnapAndNameSuite{})
+
+func (s *SnapAndNameSuite) TestUnmarshalFlag(c *C) {
+ var sn SnapAndName
+ // Typical
+ err := sn.UnmarshalFlag("snap:name")
+ c.Assert(err, IsNil)
+ c.Check(sn.Snap, Equals, "snap")
+ c.Check(sn.Name, Equals, "name")
+ // Abbreviated
+ err = sn.UnmarshalFlag("snap")
+ c.Assert(err, IsNil)
+ c.Check(sn.Snap, Equals, "snap")
+ c.Check(sn.Name, Equals, "")
+ // Invalid
+ for _, input := range []string{
+ "snap:", // Empty name, should be spelled as "snap"
+ ":", // Both snap and name empty, makes no sense
+ "snap:name:more", // Name containing :, probably a typo
+ "", // Empty input
+ } {
+ err = sn.UnmarshalFlag(input)
+ c.Assert(err, ErrorMatches, `invalid value: ".*" \(want snap:name or snap\)`)
+ c.Check(sn.Snap, Equals, "")
+ c.Check(sn.Name, Equals, "")
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strings"
+ "unicode"
+
+ "github.com/jessevdk/go-flags"
+
+ "golang.org/x/crypto/ssh/terminal"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/store"
+)
+
+func init() {
+ // set User-Agent for when 'snap' talks to the store directly (snap download etc...)
+ store.SetUserAgentFromVersion(cmd.Version, "snap")
+}
+
+// Standard streams, redirected for testing.
+var (
+ Stdin io.Reader = os.Stdin
+ Stdout io.Writer = os.Stdout
+ Stderr io.Writer = os.Stderr
+ ReadPassword = terminal.ReadPassword
+)
+
+type options struct {
+ Version func() `long:"version"`
+}
+
+type argDesc struct {
+ name string
+ desc string
+}
+
+var optionsData options
+
+// ErrExtraArgs is returned if extra arguments to a command are found
+var ErrExtraArgs = fmt.Errorf(i18n.G("too many arguments for command"))
+
+// cmdInfo holds information needed to call parser.AddCommand(...).
+type cmdInfo struct {
+ name, shortHelp, longHelp string
+ builder func() flags.Commander
+ hidden bool
+ optDescs map[string]string
+ argDescs []argDesc
+}
+
+// commands holds information about all non-experimental commands.
+var commands []*cmdInfo
+
+// experimentalCommands holds information about all experimental commands.
+var experimentalCommands []*cmdInfo
+
+// addCommand replaces parser.addCommand() in a way that is compatible with
+// re-constructing a pristine parser.
+func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
+ info := &cmdInfo{
+ name: name,
+ shortHelp: shortHelp,
+ longHelp: longHelp,
+ builder: builder,
+ optDescs: optDescs,
+ argDescs: argDescs,
+ }
+ commands = append(commands, info)
+ return info
+}
+
+// addExperimentalCommand replaces parser.addCommand() in a way that is
+// compatible with re-constructing a pristine parser. It is meant for
+// adding experimental commands.
+func addExperimentalCommand(name, shortHelp, longHelp string, builder func() flags.Commander) *cmdInfo {
+ info := &cmdInfo{
+ name: name,
+ shortHelp: shortHelp,
+ longHelp: longHelp,
+ builder: builder,
+ }
+ experimentalCommands = append(experimentalCommands, info)
+ return info
+}
+
+type parserSetter interface {
+ setParser(*flags.Parser)
+}
+
+func lintDesc(cmdName, optName, desc, origDesc string) {
+ if len(optName) == 0 {
+ logger.Panicf("option on %q has no name", cmdName)
+ }
+ if len(origDesc) != 0 {
+ logger.Panicf("description of %s's %q of %q set from tag (=> no i18n)", cmdName, optName, origDesc)
+ }
+ if len(desc) > 0 {
+ if !unicode.IsUpper(([]rune)(desc)[0]) {
+ logger.Panicf("description of %s's %q not uppercase: %q", cmdName, optName, desc)
+ }
+ }
+}
+
+func lintArg(cmdName, optName, desc, origDesc string) {
+ lintDesc(cmdName, optName, desc, origDesc)
+ if optName[0] != '<' || optName[len(optName)-1] != '>' {
+ logger.Panicf("argument %q's %q should have <>s", cmdName, optName)
+ }
+}
+
+// Parser creates and populates a fresh parser.
+// Since commands have local state a fresh parser is required to isolate tests
+// from each other.
+func Parser() *flags.Parser {
+ optionsData.Version = func() {
+ printVersions()
+ panic(&exitStatus{0})
+ }
+ parser := flags.NewParser(&optionsData, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption)
+ parser.ShortDescription = i18n.G("Tool to interact with snaps")
+ parser.LongDescription = i18n.G(`
+Install, configure, refresh and remove snap packages. Snaps are
+'universal' packages that work across many different Linux systems,
+enabling secure distribution of the latest apps and utilities for
+cloud, servers, desktops and the internet of things.
+
+This is the CLI for snapd, a background service that takes care of
+snaps on the system. Start with 'snap list' to see installed snaps.
+`)
+ parser.FindOptionByLongName("version").Description = i18n.G("Print the version and exit")
+
+ // Add all regular commands
+ for _, c := range commands {
+ obj := c.builder()
+ if x, ok := obj.(parserSetter); ok {
+ x.setParser(parser)
+ }
+
+ cmd, err := parser.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj)
+ if err != nil {
+ logger.Panicf("cannot add command %q: %v", c.name, err)
+ }
+ cmd.Hidden = c.hidden
+
+ opts := cmd.Options()
+ if c.optDescs != nil && len(opts) != len(c.optDescs) {
+ logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs))
+ }
+ for _, opt := range opts {
+ name := opt.LongName
+ if name == "" {
+ name = string(opt.ShortName)
+ }
+ desc, ok := c.optDescs[name]
+ if !(c.optDescs == nil || ok) {
+ logger.Panicf("%s missing description for %s", c.name, name)
+ }
+ lintDesc(c.name, name, desc, opt.Description)
+ if desc != "" {
+ opt.Description = desc
+ }
+ }
+
+ args := cmd.Args()
+ if c.argDescs != nil && len(args) != len(c.argDescs) {
+ logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs))
+ }
+ for i, arg := range args {
+ name, desc := arg.Name, ""
+ if c.argDescs != nil {
+ name = c.argDescs[i].name
+ desc = c.argDescs[i].desc
+ }
+ lintArg(c.name, name, desc, arg.Description)
+ arg.Name = name
+ arg.Description = desc
+ }
+ }
+ return parser
+}
+
+// ClientConfig is the configuration of the Client used by all commands.
+var ClientConfig client.Config
+
+// Client returns a new client using ClientConfig as configuration.
+func Client() *client.Client {
+ return client.New(&ClientConfig)
+}
+
+func init() {
+ err := logger.SimpleSetup()
+ if err != nil {
+ fmt.Fprintf(Stderr, i18n.G("WARNING: failed to activate logging: %v\n"), err)
+ }
+}
+
+func resolveApp(snapApp string) (string, error) {
+ target, err := os.Readlink(filepath.Join(dirs.SnapBinariesDir, snapApp))
+ if err != nil {
+ return "", err
+ }
+ if filepath.Base(target) == target { // alias pointing to an app command in /snap/bin
+ return target, nil
+ }
+ return snapApp, nil
+}
+
+func main() {
+ cmd.ExecInCoreSnap()
+
+ // magic \o/
+ snapApp := filepath.Base(os.Args[0])
+ if osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, snapApp)) {
+ var err error
+ snapApp, err = resolveApp(snapApp)
+ if err != nil {
+ fmt.Fprintf(Stderr, i18n.G("cannot resolve snap app %q: %v"), snapApp, err)
+ os.Exit(46)
+ }
+ cmd := &cmdRun{}
+ args := []string{snapApp}
+ args = append(args, os.Args[1:]...)
+ // this will call syscall.Exec() so it does not return
+ // *unless* there is an error, i.e. we setup a wrong
+ // symlink (or syscall.Exec() fails for strange reasons)
+ err = cmd.Execute(args)
+ fmt.Fprintf(Stderr, i18n.G("internal error, please report: running %q failed: %v\n"), snapApp, err)
+ os.Exit(46)
+ }
+
+ defer func() {
+ if v := recover(); v != nil {
+ if e, ok := v.(*exitStatus); ok {
+ os.Exit(e.code)
+ }
+ panic(v)
+ }
+ }()
+
+ // no magic /o\
+ if err := run(); err != nil {
+ fmt.Fprintf(Stderr, i18n.G("error: %v\n"), err)
+ os.Exit(1)
+ }
+}
+
+type exitStatus struct {
+ code int
+}
+
+func (e *exitStatus) Error() string {
+ return fmt.Sprintf("internal error: exitStatus{%d} being handled as normal error", e.code)
+}
+
+func run() error {
+ parser := Parser()
+ _, err := parser.Parse()
+ if err != nil {
+ if e, ok := err.(*flags.Error); ok {
+ if e.Type == flags.ErrHelp || e.Type == flags.ErrCommandRequired {
+ if parser.Command.Active != nil && parser.Command.Active.Name == "help" {
+ parser.Command.Active = nil
+ }
+ parser.WriteHelp(Stdout)
+ return nil
+ }
+ if e.Type == flags.ErrUnknownCommand {
+ return fmt.Errorf(i18n.G(`unknown command %q, see "snap --help"`), os.Args[1])
+ }
+ }
+ if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindLoginRequired {
+ u, _ := user.Current()
+ if u != nil && u.Username == "root" {
+ return fmt.Errorf(i18n.G(`%s (see "snap login --help")`), e.Message)
+ }
+
+ // TRANSLATORS: %s will be a message along the lines of "login required"
+ return fmt.Errorf(i18n.G(`%s (try with sudo)`), e.Message)
+ }
+ }
+
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "golang.org/x/crypto/ssh/terminal"
+
+ "github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type BaseSnapSuite struct {
+ testutil.BaseTest
+ stdin *bytes.Buffer
+ stdout *bytes.Buffer
+ stderr *bytes.Buffer
+ password string
+
+ AuthFile string
+}
+
+func (s *BaseSnapSuite) readPassword(fd int) ([]byte, error) {
+ return []byte(s.password), nil
+}
+
+func (s *BaseSnapSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+ s.stdin = bytes.NewBuffer(nil)
+ s.stdout = bytes.NewBuffer(nil)
+ s.stderr = bytes.NewBuffer(nil)
+ s.password = ""
+
+ snap.Stdin = s.stdin
+ snap.Stdout = s.stdout
+ snap.Stderr = s.stderr
+ snap.ReadPassword = s.readPassword
+ s.AuthFile = filepath.Join(c.MkDir(), "json")
+ os.Setenv(TestAuthFileEnvKey, s.AuthFile)
+}
+
+func (s *BaseSnapSuite) TearDownTest(c *C) {
+ snap.Stdin = os.Stdin
+ snap.Stdout = os.Stdout
+ snap.Stderr = os.Stderr
+ snap.ReadPassword = terminal.ReadPassword
+
+ c.Assert(s.AuthFile == "", Equals, false)
+ err := os.Unsetenv(TestAuthFileEnvKey)
+ c.Assert(err, IsNil)
+ s.BaseTest.TearDownTest(c)
+}
+
+func (s *BaseSnapSuite) Stdout() string {
+ return s.stdout.String()
+}
+
+func (s *BaseSnapSuite) Stderr() string {
+ return s.stderr.String()
+}
+
+func (s *BaseSnapSuite) ResetStdStreams() {
+ s.stdin.Reset()
+ s.stdout.Reset()
+ s.stderr.Reset()
+}
+
+func (s *BaseSnapSuite) RedirectClientToTestServer(handler func(http.ResponseWriter, *http.Request)) {
+ server := httptest.NewServer(http.HandlerFunc(handler))
+ s.BaseTest.AddCleanup(func() { server.Close() })
+ snap.ClientConfig.BaseURL = server.URL
+ s.BaseTest.AddCleanup(func() { snap.ClientConfig.BaseURL = "" })
+}
+
+func (s *BaseSnapSuite) Login(c *C) {
+ err := osutil.AtomicWriteFile(s.AuthFile, []byte(TestAuthFileContents), 0600, 0)
+ c.Assert(err, IsNil)
+}
+
+func (s *BaseSnapSuite) Logout(c *C) {
+ if osutil.FileExists(s.AuthFile) {
+ c.Assert(os.Remove(s.AuthFile), IsNil)
+ }
+}
+
+type SnapSuite struct {
+ BaseSnapSuite
+}
+
+var _ = Suite(&SnapSuite{})
+
+// DecodedRequestBody returns the JSON-decoded body of the request.
+func DecodedRequestBody(c *C, r *http.Request) map[string]interface{} {
+ var body map[string]interface{}
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&body)
+ c.Assert(err, IsNil)
+ return body
+}
+
+// EncodeResponseBody writes JSON-serialized body to the response writer.
+func EncodeResponseBody(c *C, w http.ResponseWriter, body interface{}) {
+ encoder := json.NewEncoder(w)
+ err := encoder.Encode(body)
+ c.Assert(err, IsNil)
+}
+
+func mockArgs(args ...string) (restore func()) {
+ old := os.Args
+ os.Args = args
+ return func() { os.Args = old }
+}
+
+func mockVersion(v string) (restore func()) {
+ old := cmd.Version
+ cmd.Version = v
+ return func() { cmd.Version = old }
+}
+
+const TestAuthFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME"
+const TestAuthFileContents = `{"id":123,"email":"hello@mail.com","macaroon":"MDAxM2xvY2F0aW9uIHNuYXBkCjAwMTJpZGVudGlmaWVyIDQzCjAwMmZzaWduYXR1cmUg5RfMua72uYop4t3cPOBmGUuaoRmoDH1HV62nMJq7eqAK"}`
+
+func (s *SnapSuite) TestErrorResult(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "cannot do something"}}`)
+ })
+
+ restore := mockArgs("snap", "install", "foo")
+ defer restore()
+
+ err := snap.RunMain()
+ c.Assert(err, ErrorMatches, `cannot do something`)
+}
+
+func (s *SnapSuite) TestAccessDeniedHint(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "access denied", "kind": "login-required"}, "status-code": 401}`)
+ })
+
+ restore := mockArgs("snap", "install", "foo")
+ defer restore()
+
+ err := snap.RunMain()
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, `access denied (try with sudo)`)
+}
+
+func (s *SnapSuite) TestExtraArgs(c *C) {
+ restore := mockArgs("snap", "abort", "1", "xxx", "zzz")
+ defer restore()
+
+ err := snap.RunMain()
+ c.Assert(err, ErrorMatches, `too many arguments for command`)
+}
+
+func (s *SnapSuite) TestVersionOnClassic(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`)
+ })
+ restore := mockArgs("snap", "--version")
+ defer restore()
+ restore = mockVersion("4.56")
+ defer restore()
+
+ c.Assert(func() { snap.RunMain() }, PanicMatches, `internal error: exitStatus\{0\} .*`)
+ c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestVersionOnAllSnap(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`)
+ })
+ restore := mockArgs("snap", "--version")
+ defer restore()
+ restore = mockVersion("4.56")
+ defer restore()
+
+ c.Assert(func() { snap.RunMain() }, PanicMatches, `internal error: exitStatus\{0\} .*`)
+ c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestUnknownCommand(c *C) {
+ restore := mockArgs("snap", "unknowncmd")
+ defer restore()
+
+ err := snap.RunMain()
+ c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see "snap --help"`)
+}
+
+func (s *SnapSuite) TestResolveApp(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("/")
+
+ err := os.MkdirAll(dirs.SnapBinariesDir, 0755)
+ c.Assert(err, IsNil)
+
+ // "wrapper" symlinks
+ err = os.Symlink("/usr/bin/snap", filepath.Join(dirs.SnapBinariesDir, "foo"))
+ c.Assert(err, IsNil)
+ err = os.Symlink("/usr/bin/snap", filepath.Join(dirs.SnapBinariesDir, "foo.bar"))
+ c.Assert(err, IsNil)
+
+ // alias symlinks
+ err = os.Symlink("foo", filepath.Join(dirs.SnapBinariesDir, "foo_"))
+ c.Assert(err, IsNil)
+ err = os.Symlink("foo.bar", filepath.Join(dirs.SnapBinariesDir, "foo_bar-1"))
+ c.Assert(err, IsNil)
+
+ snapApp, err := snap.ResolveApp("foo")
+ c.Assert(err, IsNil)
+ c.Check(snapApp, Equals, "foo")
+
+ snapApp, err = snap.ResolveApp("foo.bar")
+ c.Assert(err, IsNil)
+ c.Check(snapApp, Equals, "foo.bar")
+
+ snapApp, err = snap.ResolveApp("foo_")
+ c.Assert(err, IsNil)
+ c.Check(snapApp, Equals, "foo")
+
+ snapApp, err = snap.ResolveApp("foo_bar-1")
+ c.Assert(err, IsNil)
+ c.Check(snapApp, Equals, "foo.bar")
+
+ _, err = snap.ResolveApp("baz")
+ c.Check(err, NotNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/snap"
+)
+
+func getPriceString(prices map[string]float64, suggestedCurrency, status string) string {
+ price, currency, err := getPrice(prices, suggestedCurrency)
+
+ // If there are no prices, then the snap is free
+ if err != nil {
+ return ""
+ }
+
+ // If the snap is priced, but has been purchased
+ if status == "available" {
+ return i18n.G("bought")
+ }
+
+ return formatPrice(price, currency)
+}
+
+func formatPrice(val float64, currency string) string {
+ return fmt.Sprintf("%.2f%s", val, currency)
+}
+
+// Notes encapsulate everything that might be interesting about a
+// snap, in order to present a brief summary of it.
+type Notes struct {
+ Price string
+ Private bool
+ DevMode bool
+ JailMode bool
+ Classic bool
+ TryMode bool
+ Disabled bool
+ Broken bool
+}
+
+func NotesFromChannelSnapInfo(ref *snap.ChannelSnapInfo) *Notes {
+ return &Notes{
+ DevMode: ref.Confinement == client.DevModeConfinement,
+ Classic: ref.Confinement == client.ClassicConfinement,
+ }
+}
+
+func NotesFromRemote(snap *client.Snap, resInfo *client.ResultInfo) *Notes {
+ notes := &Notes{
+ Private: snap.Private,
+ DevMode: snap.Confinement == client.DevModeConfinement,
+ Classic: snap.Confinement == client.ClassicConfinement,
+ }
+ if resInfo != nil {
+ notes.Price = getPriceString(snap.Prices, resInfo.SuggestedCurrency, snap.Status)
+ }
+
+ return notes
+}
+
+func NotesFromLocal(snap *client.Snap) *Notes {
+ return &Notes{
+ Private: snap.Private,
+ DevMode: !snap.JailMode && (snap.DevMode || snap.Confinement == client.DevModeConfinement),
+ Classic: !snap.JailMode && (snap.Confinement == client.ClassicConfinement),
+ JailMode: snap.JailMode,
+ TryMode: snap.TryMode,
+ Disabled: snap.Status != client.StatusActive,
+ Broken: snap.Broken != "",
+ }
+}
+
+func NotesFromInfo(info *snap.Info) *Notes {
+ return &Notes{
+ Private: info.Private,
+ DevMode: info.Confinement == client.DevModeConfinement,
+ Classic: info.Confinement == client.ClassicConfinement,
+ Broken: info.Broken != "",
+ }
+}
+
+func (n *Notes) String() string {
+ if n == nil {
+ return ""
+ }
+ var ns []string
+
+ if n.Disabled {
+ // TRANSLATORS: if possible, a single short word
+ ns = append(ns, i18n.G("disabled"))
+ }
+
+ if n.Price != "" {
+ ns = append(ns, n.Price)
+ }
+
+ if n.DevMode {
+ ns = append(ns, "devmode")
+ }
+
+ if n.JailMode {
+ ns = append(ns, "jailmode")
+ }
+
+ if n.Classic {
+ ns = append(ns, "classic")
+ }
+
+ if n.Private {
+ // TRANSLATORS: if possible, a single short word
+ ns = append(ns, i18n.G("private"))
+ }
+
+ if n.TryMode {
+ ns = append(ns, "try")
+ }
+
+ if n.Broken {
+ // TRANSLATORS: if possible, a single short word
+ ns = append(ns, i18n.G("broken"))
+ }
+
+ if len(ns) == 0 {
+ return "-"
+ }
+
+ return strings.Join(ns, ",")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+type notesSuite struct{}
+
+var _ = check.Suite(¬esSuite{})
+
+func (notesSuite) TestNoNotes(c *check.C) {
+ c.Check((&snap.Notes{}).String(), check.Equals, "-")
+}
+
+func (notesSuite) TestNotesPrice(c *check.C) {
+ c.Check((&snap.Notes{
+ Price: "3.50GBP",
+ }).String(), check.Equals, "3.50GBP")
+}
+
+func (notesSuite) TestNotesPrivate(c *check.C) {
+ c.Check((&snap.Notes{
+ Private: true,
+ }).String(), check.Equals, "private")
+}
+
+func (notesSuite) TestNotesDevMode(c *check.C) {
+ c.Check((&snap.Notes{
+ DevMode: true,
+ }).String(), check.Equals, "devmode")
+}
+
+func (notesSuite) TestNotesJailMode(c *check.C) {
+ c.Check((&snap.Notes{
+ JailMode: true,
+ }).String(), check.Equals, "jailmode")
+}
+
+func (notesSuite) TestNotesClassic(c *check.C) {
+ c.Check((&snap.Notes{
+ Classic: true,
+ }).String(), check.Equals, "classic")
+}
+
+func (notesSuite) TestNotesTryMode(c *check.C) {
+ c.Check((&snap.Notes{
+ TryMode: true,
+ }).String(), check.Equals, "try")
+}
+
+func (notesSuite) TestNotesDisabled(c *check.C) {
+ c.Check((&snap.Notes{
+ Disabled: true,
+ }).String(), check.Equals, "disabled")
+}
+
+func (notesSuite) TestNotesBroken(c *check.C) {
+ c.Check((&snap.Notes{
+ Broken: true,
+ }).String(), check.Equals, "broken")
+}
+
+func (notesSuite) TestNotesNothing(c *check.C) {
+ c.Check((&snap.Notes{}).String(), check.Equals, "-")
+}
+
+func (notesSuite) TestNotesTwo(c *check.C) {
+ c.Check((&snap.Notes{
+ DevMode: true,
+ Broken: true,
+ }).String(), check.Matches, "(devmode,broken|broken,devmode)")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/client"
+)
+
+var clientConfig client.Config
+
+func main() {
+ stdout, stderr, err := run()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %s\n", err)
+ os.Exit(1)
+ }
+
+ if stdout != nil {
+ os.Stdout.Write(stdout)
+ }
+
+ if stderr != nil {
+ os.Stderr.Write(stderr)
+ }
+}
+
+func run() (stdout, stderr []byte, err error) {
+ cli := client.New(&clientConfig)
+
+ return cli.RunSnapctl(&client.SnapCtlOptions{
+ ContextID: os.Getenv("SNAP_CONTEXT"),
+ Args: os.Args[1:],
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
+ "github.com/snapcore/snapd/client"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type snapctlSuite struct {
+ server *httptest.Server
+ oldArgs []string
+ expectedContextID string
+ expectedArgs []string
+}
+
+var _ = Suite(&snapctlSuite{})
+
+func (s *snapctlSuite) SetUpTest(c *C) {
+ os.Setenv("SNAP_CONTEXT", "snap-context-test")
+ n := 0
+ s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Assert(r.Method, Equals, "POST")
+ c.Assert(r.URL.Path, Equals, "/v2/snapctl")
+
+ var snapctlOptions client.SnapCtlOptions
+ decoder := json.NewDecoder(r.Body)
+ c.Assert(decoder.Decode(&snapctlOptions), IsNil)
+ c.Assert(snapctlOptions.ContextID, Equals, s.expectedContextID)
+ c.Assert(snapctlOptions.Args, DeepEquals, s.expectedArgs)
+
+ fmt.Fprintln(w, `{"type": "sync", "result": {"stdout": "test stdout", "stderr": "test stderr"}}`)
+ default:
+ c.Fatalf("expected to get 1 request, now on %d", n+1)
+ }
+
+ n++
+ }))
+ clientConfig.BaseURL = s.server.URL
+ s.oldArgs = os.Args
+ os.Args = []string{"snapctl"}
+ s.expectedContextID = "snap-context-test"
+ s.expectedArgs = []string{}
+}
+
+func (s *snapctlSuite) TearDownTest(c *C) {
+ os.Unsetenv("SNAP_CONTEXT")
+ clientConfig.BaseURL = ""
+ s.server.Close()
+ os.Args = s.oldArgs
+}
+
+func (s *snapctlSuite) TestSnapctl(c *C) {
+ stdout, stderr, err := run()
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "test stdout")
+ c.Check(string(stderr), Equals, "test stderr")
+}
+
+func (s *snapctlSuite) TestSnapctlWithArgs(c *C) {
+ os.Args = []string{"snapctl", "foo", "--bar"}
+
+ s.expectedArgs = []string{"foo", "--bar"}
+ stdout, stderr, err := run()
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "test stdout")
+ c.Check(string(stderr), Equals, "test stderr")
+}
+
+func (s *snapctlSuite) TestSnapctlHelp(c *C) {
+ os.Unsetenv("SNAP_CONTEXT")
+ s.expectedContextID = ""
+
+ os.Args = []string{"snapctl", "-h"}
+ s.expectedArgs = []string{"-h"}
+
+ _, _, err := run()
+ c.Check(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/daemon"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/store"
+)
+
+func init() {
+ err := logger.SimpleSetup()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %s\n", err)
+ }
+}
+
+func main() {
+ cmd.ExecInCoreSnap()
+ if err := run(); err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func run() error {
+ store.SetUserAgentFromVersion(cmd.Version)
+
+ d, err := daemon.New()
+ if err != nil {
+ return err
+ }
+ if err := d.Init(); err != nil {
+ return err
+ }
+ d.Version = cmd.Version
+
+ d.Start()
+
+ ch := make(chan os.Signal)
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+ select {
+ case sig := <-ch:
+ logger.Noticef("Exiting on %s signal.\n", sig)
+ case <-d.Dying():
+ // something called Stop()
+ }
+
+ return d.Stop()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package cmd
+
+//go:generate mkversion.sh
+
+// Version will be overwritten at build-time via mkversion.sh
+var Version = "unknown"
+
+func MockVersion(version string) (restore func()) {
+ old := Version
+ Version = version
+ return func() { Version = old }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime"
+ "mime/multipart"
+ "net/http"
+ "os"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/ifacestate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/strutil"
+)
+
+var api = []*Command{
+ rootCmd,
+ sysInfoCmd,
+ loginCmd,
+ logoutCmd,
+ appIconCmd,
+ findCmd,
+ snapsCmd,
+ snapCmd,
+ snapConfCmd,
+ interfacesCmd,
+ assertsCmd,
+ assertsFindManyCmd,
+ stateChangeCmd,
+ stateChangesCmd,
+ createUserCmd,
+ buyCmd,
+ readyToBuyCmd,
+ snapctlCmd,
+ usersCmd,
+ sectionsCmd,
+ aliasesCmd,
+}
+
+var (
+ rootCmd = &Command{
+ Path: "/",
+ GuestOK: true,
+ GET: tbd,
+ }
+
+ sysInfoCmd = &Command{
+ Path: "/v2/system-info",
+ GuestOK: true,
+ GET: sysInfo,
+ }
+
+ loginCmd = &Command{
+ Path: "/v2/login",
+ POST: loginUser,
+ }
+
+ logoutCmd = &Command{
+ Path: "/v2/logout",
+ POST: logoutUser,
+ UserOK: true,
+ }
+
+ appIconCmd = &Command{
+ Path: "/v2/icons/{name}/icon",
+ UserOK: true,
+ GET: appIconGet,
+ }
+
+ findCmd = &Command{
+ Path: "/v2/find",
+ UserOK: true,
+ GET: searchStore,
+ }
+
+ snapsCmd = &Command{
+ Path: "/v2/snaps",
+ UserOK: true,
+ GET: getSnapsInfo,
+ POST: postSnaps,
+ }
+
+ snapCmd = &Command{
+ Path: "/v2/snaps/{name}",
+ UserOK: true,
+ GET: getSnapInfo,
+ POST: postSnap,
+ }
+
+ snapConfCmd = &Command{
+ Path: "/v2/snaps/{name}/conf",
+ GET: getSnapConf,
+ PUT: setSnapConf,
+ }
+
+ interfacesCmd = &Command{
+ Path: "/v2/interfaces",
+ UserOK: true,
+ GET: getInterfaces,
+ POST: changeInterfaces,
+ }
+
+ // TODO: allow to post assertions for UserOK? they are verified anyway
+ assertsCmd = &Command{
+ Path: "/v2/assertions",
+ POST: doAssert,
+ }
+
+ assertsFindManyCmd = &Command{
+ Path: "/v2/assertions/{assertType}",
+ UserOK: true,
+ GET: assertsFindMany,
+ }
+
+ stateChangeCmd = &Command{
+ Path: "/v2/changes/{id}",
+ UserOK: true,
+ GET: getChange,
+ POST: abortChange,
+ }
+
+ stateChangesCmd = &Command{
+ Path: "/v2/changes",
+ UserOK: true,
+ GET: getChanges,
+ }
+
+ createUserCmd = &Command{
+ Path: "/v2/create-user",
+ UserOK: false,
+ POST: postCreateUser,
+ }
+
+ buyCmd = &Command{
+ Path: "/v2/buy",
+ UserOK: false,
+ POST: postBuy,
+ }
+
+ readyToBuyCmd = &Command{
+ Path: "/v2/buy/ready",
+ UserOK: false,
+ GET: readyToBuy,
+ }
+
+ snapctlCmd = &Command{
+ Path: "/v2/snapctl",
+ SnapOK: true,
+ POST: runSnapctl,
+ }
+
+ usersCmd = &Command{
+ Path: "/v2/users",
+ UserOK: false,
+ GET: getUsers,
+ }
+
+ sectionsCmd = &Command{
+ Path: "/v2/sections",
+ UserOK: true,
+ GET: getSections,
+ }
+
+ aliasesCmd = &Command{
+ Path: "/v2/aliases",
+ UserOK: true,
+ GET: getAliases,
+ POST: changeAliases,
+ }
+)
+
+func tbd(c *Command, r *http.Request, user *auth.UserState) Response {
+ return SyncResponse([]string{"TBD"}, nil)
+}
+
+func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response {
+ st := c.d.overlord.State()
+ st.Lock()
+ users, err := auth.Users(st)
+ st.Unlock()
+ if err != nil && err != state.ErrNoState {
+ return InternalError("cannot get user auth data: %s", err)
+ }
+
+ m := map[string]interface{}{
+ "series": release.Series,
+ "version": c.d.Version,
+ "os-release": release.ReleaseInfo,
+ "on-classic": release.OnClassic,
+ "managed": len(users) > 0,
+ }
+
+ // TODO: set the store-id here from the model information
+ if storeID := os.Getenv("UBUNTU_STORE_ID"); storeID != "" {
+ m["store"] = storeID
+ }
+
+ return SyncResponse(m, nil)
+}
+
+// userResponseData contains the data releated to user creation/login/query
+type userResponseData struct {
+ ID int `json:"id,omitempty"`
+ Username string `json:"username,omitempty"`
+ Email string `json:"email,omitempty"`
+ SSHKeys []string `json:"ssh-keys,omitempty"`
+
+ Macaroon string `json:"macaroon,omitempty"`
+ Discharges []string `json:"discharges,omitempty"`
+}
+
+var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString
+
+func loginUser(c *Command, r *http.Request, user *auth.UserState) Response {
+ var loginData struct {
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Password string `json:"password"`
+ Otp string `json:"otp"`
+ }
+
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&loginData); err != nil {
+ return BadRequest("cannot decode login data from request body: %v", err)
+ }
+
+ if loginData.Email == "" && isEmailish(loginData.Username) {
+ // for backwards compatibility, if no email is provided assume username is the email
+ loginData.Email = loginData.Username
+ loginData.Username = ""
+ }
+
+ if loginData.Email == "" && user != nil && user.Email != "" {
+ loginData.Email = user.Email
+ }
+
+ // the "username" needs to look a lot like an email address
+ if !isEmailish(loginData.Email) {
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: "please use a valid email address.",
+ Kind: errorKindInvalidAuthData,
+ Value: map[string][]string{"email": {"invalid"}},
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ }
+
+ macaroon, discharge, err := store.LoginUser(loginData.Email, loginData.Password, loginData.Otp)
+ switch err {
+ case store.ErrAuthenticationNeeds2fa:
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Kind: errorKindTwoFactorRequired,
+ Message: err.Error(),
+ },
+ Status: http.StatusUnauthorized,
+ }, nil)
+ case store.Err2faFailed:
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Kind: errorKindTwoFactorFailed,
+ Message: err.Error(),
+ },
+ Status: http.StatusUnauthorized,
+ }, nil)
+ default:
+ if err, ok := err.(store.ErrInvalidAuthData); ok {
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindInvalidAuthData,
+ Value: err,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ }
+ return Unauthorized(err.Error())
+ case nil:
+ // continue
+ }
+ overlord := c.d.overlord
+ state := overlord.State()
+ state.Lock()
+ if user != nil {
+ // local user logged-in, set its store macaroons
+ user.StoreMacaroon = macaroon
+ user.StoreDischarges = []string{discharge}
+ err = auth.UpdateUser(state, user)
+ } else {
+ user, err = auth.NewUser(state, loginData.Username, loginData.Email, macaroon, []string{discharge})
+ }
+ state.Unlock()
+ if err != nil {
+ return InternalError("cannot persist authentication details: %v", err)
+ }
+
+ result := userResponseData{
+ ID: user.ID,
+ Username: user.Username,
+ Email: user.Email,
+ Macaroon: user.Macaroon,
+ Discharges: user.Discharges,
+ }
+ return SyncResponse(result, nil)
+}
+
+func logoutUser(c *Command, r *http.Request, user *auth.UserState) Response {
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ if user == nil {
+ return BadRequest("not logged in")
+ }
+ err := auth.RemoveUser(state, user.ID)
+ if err != nil {
+ return InternalError(err.Error())
+ }
+
+ return SyncResponse(nil, nil)
+}
+
+// UserFromRequest extracts user information from request and return the respective user in state, if valid
+// It requires the state to be locked
+func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) {
+ // extract macaroons data from request
+ header := req.Header.Get("Authorization")
+ if header == "" {
+ return nil, auth.ErrInvalidAuth
+ }
+
+ authorizationData := strings.SplitN(header, " ", 2)
+ if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" {
+ return nil, fmt.Errorf("authorization header misses Macaroon prefix")
+ }
+
+ var macaroon string
+ var discharges []string
+ for _, field := range strings.Split(authorizationData[1], ",") {
+ field := strings.TrimSpace(field)
+ if strings.HasPrefix(field, `root="`) {
+ macaroon = strings.TrimSuffix(field[6:], `"`)
+ }
+ if strings.HasPrefix(field, `discharge="`) {
+ discharges = append(discharges, strings.TrimSuffix(field[11:], `"`))
+ }
+ }
+
+ if macaroon == "" {
+ return nil, fmt.Errorf("invalid authorization header")
+ }
+
+ user, err := auth.CheckMacaroon(st, macaroon, discharges)
+ return user, err
+}
+
+var muxVars = mux.Vars
+
+func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response {
+ vars := muxVars(r)
+ name := vars["name"]
+
+ about, err := localSnapInfo(c.d.overlord.State(), name)
+ if err != nil {
+ if err == errNoSnap {
+ return NotFound("cannot find %q snap", name)
+ }
+
+ return InternalError("%v", err)
+ }
+
+ route := c.d.router.Get(c.Path)
+ if route == nil {
+ return InternalError("cannot find route for %q snap", name)
+ }
+
+ url, err := route.URL("name", name)
+ if err != nil {
+ return InternalError("cannot build URL for %q snap: %v", name, err)
+ }
+
+ result := webify(mapLocal(about), url.String())
+
+ return SyncResponse(result, nil)
+}
+
+func webify(result map[string]interface{}, resource string) map[string]interface{} {
+ result["resource"] = resource
+
+ icon, ok := result["icon"].(string)
+ if !ok || icon == "" || strings.HasPrefix(icon, "http") {
+ return result
+ }
+ result["icon"] = ""
+
+ route := appIconCmd.d.router.Get(appIconCmd.Path)
+ if route != nil {
+ name, _ := result["name"].(string)
+ url, err := route.URL("name", name)
+ if err == nil {
+ result["icon"] = url.String()
+ }
+ }
+
+ return result
+}
+
+func getStore(c *Command) snapstate.StoreService {
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ return snapstate.Store(st)
+}
+
+func getSections(c *Command, r *http.Request, user *auth.UserState) Response {
+ route := c.d.router.Get(snapCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for snaps")
+ }
+
+ theStore := getStore(c)
+
+ sections, err := theStore.Sections(user)
+ switch err {
+ case nil:
+ // pass
+ case store.ErrEmptyQuery, store.ErrBadQuery:
+ return BadRequest("%v", err)
+ case store.ErrUnauthenticated:
+ return Unauthorized("%v", err)
+ default:
+ return InternalError("%v", err)
+ }
+
+ return SyncResponse(sections, &Meta{})
+}
+
+func searchStore(c *Command, r *http.Request, user *auth.UserState) Response {
+ route := c.d.router.Get(snapCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for snaps")
+ }
+ query := r.URL.Query()
+ q := query.Get("q")
+ section := query.Get("section")
+ name := query.Get("name")
+ private := false
+ prefix := false
+
+ if name != "" {
+ if q != "" {
+ return BadRequest("cannot use 'q' and 'name' together")
+ }
+
+ if name[len(name)-1] != '*' {
+ return findOne(c, r, user, name)
+ }
+
+ prefix = true
+ q = name[:len(name)-1]
+ }
+
+ if sel := query.Get("select"); sel != "" {
+ switch sel {
+ case "refresh":
+ if prefix {
+ return BadRequest("cannot use 'name' with 'select=refresh'")
+ }
+ if q != "" {
+ return BadRequest("cannot use 'q' with 'select=refresh'")
+ }
+ return storeUpdates(c, r, user)
+ case "private":
+ private = true
+ }
+ }
+
+ theStore := getStore(c)
+ found, err := theStore.Find(&store.Search{
+ Query: q,
+ Section: section,
+ Private: private,
+ Prefix: prefix,
+ }, user)
+ switch err {
+ case nil:
+ // pass
+ case store.ErrEmptyQuery, store.ErrBadQuery:
+ return BadRequest("%v", err)
+ case store.ErrUnauthenticated:
+ return Unauthorized(err.Error())
+ default:
+ return InternalError("%v", err)
+ }
+
+ meta := &Meta{
+ SuggestedCurrency: theStore.SuggestedCurrency(),
+ Sources: []string{"store"},
+ }
+
+ return sendStorePackages(route, meta, found)
+}
+
+func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response {
+ if err := snap.ValidateName(name); err != nil {
+ return BadRequest(err.Error())
+ }
+
+ theStore := getStore(c)
+ spec := store.SnapSpec{
+ Name: name,
+ Channel: "",
+ Revision: snap.R(0),
+ }
+ snapInfo, err := theStore.SnapInfo(spec, user)
+ if err != nil {
+ return InternalError("%v", err)
+ }
+
+ meta := &Meta{
+ SuggestedCurrency: theStore.SuggestedCurrency(),
+ Sources: []string{"store"},
+ }
+
+ results := make([]*json.RawMessage, 1)
+ data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String()))
+ if err != nil {
+ return InternalError(err.Error())
+ }
+ results[0] = (*json.RawMessage)(&data)
+ return SyncResponse(results, meta)
+}
+
+func shouldSearchStore(r *http.Request) bool {
+ // we should jump to the old behaviour iff q is given, or if
+ // sources is given and either empty or contains the word
+ // 'store'. Otherwise, local results only.
+
+ query := r.URL.Query()
+
+ if _, ok := query["q"]; ok {
+ logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL)
+ return true
+ }
+
+ if src, ok := query["sources"]; ok {
+ logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL)
+ if len(src) == 0 || strings.Contains(src[0], "store") {
+ return true
+ }
+ }
+
+ return false
+}
+
+func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response {
+ route := c.d.router.Get(snapCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for snaps")
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ updates, err := snapstateRefreshCandidates(state, user)
+ state.Unlock()
+ if err != nil {
+ return InternalError("cannot list updates: %v", err)
+ }
+
+ return sendStorePackages(route, nil, updates)
+}
+
+func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response {
+ results := make([]*json.RawMessage, 0, len(found))
+ for _, x := range found {
+ url, err := route.URL("name", x.Name())
+ if err != nil {
+ logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.Name(), x.Revision, err)
+ continue
+ }
+
+ data, err := json.Marshal(webify(mapRemote(x), url.String()))
+ if err != nil {
+ return InternalError("%v", err)
+ }
+ raw := json.RawMessage(data)
+ results = append(results, &raw)
+ }
+
+ return SyncResponse(results, meta)
+}
+
+// plural!
+func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
+
+ if shouldSearchStore(r) {
+ logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL)
+ return searchStore(c, r, user)
+ }
+
+ route := c.d.router.Get(snapCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for snaps")
+ }
+
+ var all bool
+ sel := r.URL.Query().Get("select")
+ switch sel {
+ case "all":
+ all = true
+ case "enabled", "":
+ all = false
+ default:
+ return BadRequest("invalid select parameter: %q", sel)
+ }
+ found, err := allLocalSnapInfos(c.d.overlord.State(), all)
+ if err != nil {
+ return InternalError("cannot list local snaps! %v", err)
+ }
+
+ results := make([]*json.RawMessage, len(found))
+
+ for i, x := range found {
+ name := x.info.Name()
+ rev := x.info.Revision
+
+ url, err := route.URL("name", name)
+ if err != nil {
+ logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err)
+ continue
+ }
+
+ data, err := json.Marshal(webify(mapLocal(x), url.String()))
+ if err != nil {
+ return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err)
+ }
+ raw := json.RawMessage(data)
+ results[i] = &raw
+ }
+
+ return SyncResponse(results, &Meta{Sources: []string{"local"}})
+}
+
+func resultHasType(r map[string]interface{}, allowedTypes []string) bool {
+ for _, t := range allowedTypes {
+ if r["type"] == t {
+ return true
+ }
+ }
+ return false
+}
+
+// licenseData holds details about the snap license, and may be
+// marshaled back as an error when the license agreement is pending,
+// and is expected as input to accept (or not) that license
+// agreement. As such, its field names are part of the API.
+type licenseData struct {
+ Intro string `json:"intro"`
+ License string `json:"license"`
+ Agreed bool `json:"agreed"`
+}
+
+func (*licenseData) Error() string {
+ return "license agreement required"
+}
+
+type snapInstruction struct {
+ progress.NullProgress
+ Action string `json:"action"`
+ Channel string `json:"channel"`
+ Revision snap.Revision `json:"revision"`
+ DevMode bool `json:"devmode"`
+ JailMode bool `json:"jailmode"`
+ Classic bool `json:"classic"`
+ IgnoreValidation bool `json:"ignore-validation"`
+ // dropping support temporarely until flag confusion is sorted,
+ // this isn't supported by client atm anyway
+ LeaveOld bool `json:"temp-dropped-leave-old"`
+ License *licenseData `json:"license"`
+ Snaps []string `json:"snaps"`
+
+ // The fields below should not be unmarshalled into. Do not export them.
+ userID int
+}
+
+var (
+ snapstateCoreInfo = snapstate.CoreInfo
+ snapstateInstall = snapstate.Install
+ snapstateInstallPath = snapstate.InstallPath
+ snapstateRefreshCandidates = snapstate.RefreshCandidates
+ snapstateTryPath = snapstate.TryPath
+ snapstateUpdate = snapstate.Update
+ snapstateUpdateMany = snapstate.UpdateMany
+ snapstateInstallMany = snapstate.InstallMany
+ snapstateRemoveMany = snapstate.RemoveMany
+
+ assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations
+)
+
+func ensureStateSoonImpl(st *state.State) {
+ st.EnsureBefore(0)
+}
+
+var ensureStateSoon = ensureStateSoonImpl
+
+var errNothingToInstall = errors.New("nothing to install")
+
+const oldDefaultSnapCoreName = "ubuntu-core"
+const defaultCoreSnapName = "core"
+
+func ensureUbuntuCore(st *state.State, targetSnap string, userID int) (*state.TaskSet, error) {
+ if targetSnap == defaultCoreSnapName || targetSnap == oldDefaultSnapCoreName {
+ return nil, errNothingToInstall
+ }
+
+ _, err := snapstateCoreInfo(st)
+ if err != state.ErrNoState {
+ return nil, err
+ }
+
+ return snapstateInstall(st, defaultCoreSnapName, "stable", snap.R(0), userID, snapstate.Flags{})
+}
+
+func withEnsureUbuntuCore(st *state.State, targetSnap string, userID int, install func() (*state.TaskSet, error)) ([]*state.TaskSet, error) {
+ ubuCoreTs, err := ensureUbuntuCore(st, targetSnap, userID)
+ if err != nil && err != errNothingToInstall {
+ return nil, err
+ }
+
+ ts, err := install()
+ if err != nil {
+ return nil, err
+ }
+
+ // ensure main install waits on ubuntu core install
+ if ubuCoreTs != nil {
+ ts.WaitAll(ubuCoreTs)
+ return []*state.TaskSet{ubuCoreTs, ts}, nil
+ }
+
+ return []*state.TaskSet{ts}, nil
+}
+
+var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together")
+var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together")
+var errNoJailMode = errors.New("this system cannot honour the jailmode flag")
+
+func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) {
+ flags := snapstate.Flags{}
+ devModeOS := release.ReleaseInfo.ForceDevMode()
+ switch {
+ case jailMode && devModeOS:
+ return flags, errNoJailMode
+ case jailMode && devMode:
+ return flags, errDevJailModeConflict
+ case devMode && classic:
+ return flags, errClassicDevmodeConflict
+ }
+ // NOTE: jailmode and classic are allowed together. In that setting,
+ // jailmode overrides classic and the app gets regular (non-classic)
+ // confinement.
+ flags.JailMode = jailMode
+ flags.Classic = classic
+ flags.DevMode = devMode || devModeOS && !classic
+ return flags, nil
+}
+
+func snapUpdateMany(inst *snapInstruction, st *state.State) (msg string, updated []string, tasksets []*state.TaskSet, err error) {
+ // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly
+ if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
+ return "", nil, nil, err
+ }
+
+ updated, tasksets, err = snapstateUpdateMany(st, inst.Snaps, inst.userID)
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ switch len(updated) {
+ case 0:
+ // not really needed but be paranoid
+ if len(inst.Snaps) != 0 {
+ return "", nil, nil, fmt.Errorf("internal error: when asking for a refresh of %s no update was found but no error was generated", strutil.Quoted(inst.Snaps))
+ }
+ // FIXME: instead don't generated a change(?) at all
+ msg = fmt.Sprintf(i18n.G("Refresh all snaps: no updates"))
+ case 1:
+ msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0])
+ default:
+ quoted := strutil.Quoted(updated)
+ // TRANSLATORS: the %s is a comma-separated list of quoted snap names
+ msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted)
+ }
+
+ return msg, updated, tasksets, nil
+}
+
+func snapInstallMany(inst *snapInstruction, st *state.State) (msg string, installed []string, tasksets []*state.TaskSet, err error) {
+ installed, tasksets, err = snapstateInstallMany(st, inst.Snaps, inst.userID)
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ switch len(inst.Snaps) {
+ case 0:
+ return "", nil, nil, fmt.Errorf("cannot install zero snaps")
+ case 1:
+ msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0])
+ default:
+ quoted := strutil.Quoted(inst.Snaps)
+ // TRANSLATORS: the %s is a comma-separated list of quoted snap names
+ msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted)
+ }
+
+ return msg, installed, tasksets, nil
+}
+
+func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ flags, err := modeFlags(inst.DevMode, inst.JailMode, inst.Classic)
+ if err != nil {
+ return "", nil, err
+ }
+
+ logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision)
+
+ tsets, err := withEnsureUbuntuCore(st, inst.Snaps[0], inst.userID,
+ func() (*state.TaskSet, error) {
+ return snapstateInstall(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags)
+ },
+ )
+ if err != nil {
+ return "", nil, err
+ }
+
+ msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0])
+ if inst.Channel != "stable" && inst.Channel != "" {
+ msg = fmt.Sprintf(i18n.G("Install %q snap from %q channel"), inst.Snaps[0], inst.Channel)
+ }
+ return msg, tsets, nil
+}
+
+func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ // TODO: bail if revision is given (and != current?), *or* behave as with install --revision?
+ flags, err := modeFlags(inst.DevMode, inst.JailMode, inst.Classic)
+ if err != nil {
+ return "", nil, err
+ }
+ if inst.IgnoreValidation {
+ flags.IgnoreValidation = true
+ }
+
+ // we need refreshed snap-declarations to enforce refresh-control as best as we can
+ if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
+ return "", nil, err
+ }
+
+ ts, err := snapstateUpdate(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags)
+ if err != nil {
+ return "", nil, err
+ }
+
+ msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0])
+ if inst.Channel != "stable" && inst.Channel != "" {
+ msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel)
+ }
+
+ return msg, []*state.TaskSet{ts}, nil
+}
+
+func snapRemoveMany(inst *snapInstruction, st *state.State) (msg string, removed []string, tasksets []*state.TaskSet, err error) {
+ removed, tasksets, err = snapstateRemoveMany(st, inst.Snaps)
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ switch len(inst.Snaps) {
+ case 0:
+ return "", nil, nil, fmt.Errorf("cannot remove zero snaps")
+ case 1:
+ msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0])
+ default:
+ quoted := strutil.Quoted(inst.Snaps)
+ // TRANSLATORS: the %s is a comma-separated list of quoted snap names
+ msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted)
+ }
+
+ return msg, removed, tasksets, nil
+}
+
+func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision)
+ if err != nil {
+ return "", nil, err
+ }
+
+ msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0])
+ return msg, []*state.TaskSet{ts}, nil
+}
+
+func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ var ts *state.TaskSet
+
+ flags, err := modeFlags(inst.DevMode, inst.JailMode, inst.Classic)
+ if err != nil {
+ return "", nil, err
+ }
+
+ if inst.Revision.Unset() {
+ ts, err = snapstate.Revert(st, inst.Snaps[0], flags)
+ } else {
+ ts, err = snapstate.RevertToRevision(st, inst.Snaps[0], inst.Revision, flags)
+ }
+ if err != nil {
+ return "", nil, err
+ }
+
+ msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0])
+ return msg, []*state.TaskSet{ts}, nil
+}
+
+func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ if !inst.Revision.Unset() {
+ return "", nil, errors.New("enable takes no revision")
+ }
+ ts, err := snapstate.Enable(st, inst.Snaps[0])
+ if err != nil {
+ return "", nil, err
+ }
+
+ msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0])
+ return msg, []*state.TaskSet{ts}, nil
+}
+
+func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ if !inst.Revision.Unset() {
+ return "", nil, errors.New("disable takes no revision")
+ }
+ ts, err := snapstate.Disable(st, inst.Snaps[0])
+ if err != nil {
+ return "", nil, err
+ }
+
+ msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0])
+ return msg, []*state.TaskSet{ts}, nil
+}
+
+type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error)
+
+var snapInstructionDispTable = map[string]snapActionFunc{
+ "install": snapInstall,
+ "refresh": snapUpdate,
+ "remove": snapRemove,
+ "revert": snapRevert,
+ "enable": snapEnable,
+ "disable": snapDisable,
+}
+
+func (inst *snapInstruction) dispatch() snapActionFunc {
+ if len(inst.Snaps) != 1 {
+ logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps))
+ }
+ return snapInstructionDispTable[inst.Action]
+}
+
+func (inst *snapInstruction) errToResponse(err error) Response {
+ if _, ok := err.(*snap.AlreadyInstalledError); ok {
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindSnapAlreadyInstalled,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ }
+ if _, ok := err.(*snap.NotInstalledError); ok {
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindSnapNotInstalled,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ }
+ if _, ok := err.(*snap.NoUpdateAvailableError); ok {
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindSnapNoUpdateAvailable,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ }
+ return BadRequest("cannot %s %q: %v", inst.Action, inst.Snaps[0], err)
+}
+
+func postSnap(c *Command, r *http.Request, user *auth.UserState) Response {
+ route := c.d.router.Get(stateChangeCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for change")
+ }
+
+ decoder := json.NewDecoder(r.Body)
+ var inst snapInstruction
+ if err := decoder.Decode(&inst); err != nil {
+ return BadRequest("cannot decode request body into snap instruction: %v", err)
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ if user != nil {
+ inst.userID = user.ID
+ }
+
+ vars := muxVars(r)
+ inst.Snaps = []string{vars["name"]}
+
+ impl := inst.dispatch()
+ if impl == nil {
+ return BadRequest("unknown action %s", inst.Action)
+ }
+
+ msg, tsets, err := impl(&inst, state)
+ if err != nil {
+ return inst.errToResponse(err)
+ }
+
+ chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps)
+
+ ensureStateSoon(state)
+
+ return AsyncResponse(nil, &Meta{Change: chg.ID()})
+}
+
+func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change {
+ chg := st.NewChange(kind, summary)
+ for _, ts := range tsets {
+ chg.AddAll(ts)
+ }
+ if snapNames != nil {
+ chg.Set("snap-names", snapNames)
+ }
+ return chg
+}
+
+const maxReadBuflen = 1024 * 1024
+
+func trySnap(c *Command, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response {
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ if !filepath.IsAbs(trydir) {
+ return BadRequest("cannot try %q: need an absolute path", trydir)
+ }
+ if !osutil.IsDirectory(trydir) {
+ return BadRequest("cannot try %q: not a snap directory", trydir)
+ }
+
+ // the developer asked us to do this with a trusted snap dir
+ info, err := unsafeReadSnapInfo(trydir)
+ if err != nil {
+ return BadRequest("cannot read snap info for %s: %s", trydir, err)
+ }
+
+ var userID int
+ if user != nil {
+ userID = user.ID
+ }
+ tsets, err := withEnsureUbuntuCore(st, info.Name(), userID,
+ func() (*state.TaskSet, error) {
+ return snapstateTryPath(st, info.Name(), trydir, flags)
+ },
+ )
+ if err != nil {
+ return BadRequest("cannot try %s: %s", trydir, err)
+ }
+
+ msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.Name(), trydir)
+ chg := newChange(st, "try-snap", msg, tsets, []string{info.Name()})
+ chg.Set("api-data", map[string]string{"snap-name": info.Name()})
+
+ ensureStateSoon(st)
+
+ return AsyncResponse(nil, &Meta{Change: chg.ID()})
+}
+
+func isTrue(form *multipart.Form, key string) bool {
+ value := form.Value[key]
+ if len(value) == 0 {
+ return false
+ }
+ b, err := strconv.ParseBool(value[0])
+ if err != nil {
+ return false
+ }
+
+ return b
+}
+
+func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response {
+ route := c.d.router.Get(stateChangeCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for change")
+ }
+
+ decoder := json.NewDecoder(r.Body)
+ var inst snapInstruction
+ if err := decoder.Decode(&inst); err != nil {
+ return BadRequest("cannot decode request body into snap instruction: %v", err)
+ }
+
+ if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode {
+ return BadRequest("unsupported option provided for multi-snap operation")
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ if user != nil {
+ inst.userID = user.ID
+ }
+
+ var msg string
+ var affected []string
+ var tsets []*state.TaskSet
+ var err error
+ switch inst.Action {
+ case "refresh":
+ msg, affected, tsets, err = snapUpdateMany(&inst, st)
+ case "install":
+ msg, affected, tsets, err = snapInstallMany(&inst, st)
+ case "remove":
+ msg, affected, tsets, err = snapRemoveMany(&inst, st)
+ default:
+ return BadRequest("unsupported multi-snap operation %q", inst.Action)
+ }
+ if err != nil {
+ return InternalError("cannot %s %q: %v", inst.Action, inst.Snaps, err)
+ }
+
+ var chg *state.Change
+ if len(tsets) == 0 {
+ chg = st.NewChange(inst.Action+"-snap", msg)
+ chg.SetStatus(state.DoneStatus)
+ } else {
+ chg = newChange(st, inst.Action+"-snap", msg, tsets, affected)
+ ensureStateSoon(st)
+ }
+ chg.Set("api-data", map[string]interface{}{"snap-names": affected})
+
+ return AsyncResponse(nil, &Meta{Change: chg.ID()})
+}
+
+func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response {
+ contentType := r.Header.Get("Content-Type")
+
+ if contentType == "application/json" {
+ return snapsOp(c, r, user)
+ }
+
+ if !strings.HasPrefix(contentType, "multipart/") {
+ return BadRequest("unknown content type: %s", contentType)
+ }
+
+ route := c.d.router.Get(stateChangeCmd.Path)
+ if route == nil {
+ return InternalError("cannot find route for change")
+ }
+
+ // POSTs to sideload snaps must be a multipart/form-data file upload.
+ _, params, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ return BadRequest("cannot parse POST body: %v", err)
+ }
+
+ form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen)
+ if err != nil {
+ return BadRequest("cannot read POST form: %v", err)
+ }
+
+ dangerousOK := isTrue(form, "dangerous")
+ flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic"))
+ if err != nil {
+ return BadRequest(err.Error())
+ }
+ flags.RemoveSnapPath = true
+
+ if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" {
+ if len(form.Value["snap-path"]) == 0 {
+ return BadRequest("need 'snap-path' value in form")
+ }
+ return trySnap(c, r, user, form.Value["snap-path"][0], flags)
+ }
+
+ // find the file for the "snap" form field
+ var snapBody multipart.File
+ var origPath string
+out:
+ for name, fheaders := range form.File {
+ if name != "snap" {
+ continue
+ }
+ for _, fheader := range fheaders {
+ snapBody, err = fheader.Open()
+ origPath = fheader.Filename
+ if err != nil {
+ return BadRequest(`cannot open uploaded "snap" file: %v`, err)
+ }
+ defer snapBody.Close()
+
+ break out
+ }
+ }
+ defer form.RemoveAll()
+
+ if snapBody == nil {
+ return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`)
+ }
+
+ // we are in charge of the tempfile life cycle until we hand it off to the change
+ changeTriggered := false
+ // if you change this prefix, look for it in the tests
+ tmpf, err := ioutil.TempFile("", "snapd-sideload-pkg-")
+ if err != nil {
+ return InternalError("cannot create temporary file: %v", err)
+ }
+
+ tempPath := tmpf.Name()
+
+ defer func() {
+ if !changeTriggered {
+ os.Remove(tempPath)
+ }
+ }()
+
+ if _, err := io.Copy(tmpf, snapBody); err != nil {
+ return InternalError("cannot copy request into temporary file: %v", err)
+ }
+ tmpf.Sync()
+
+ if len(form.Value["snap-path"]) > 0 {
+ origPath = form.Value["snap-path"][0]
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var snapName string
+ var sideInfo *snap.SideInfo
+
+ if !dangerousOK {
+ si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st))
+ switch err {
+ case nil:
+ snapName = si.RealName
+ sideInfo = si
+ case asserts.ErrNotFound:
+ // with devmode we try to find assertions but it's ok
+ // if they are not there (implies --dangerous)
+ if !isTrue(form, "devmode") {
+ msg := "cannot find signatures with metadata for snap"
+ if origPath != "" {
+ msg = fmt.Sprintf("%s %q", msg, origPath)
+ }
+ return BadRequest(msg)
+ }
+ // TODO: set a warning if devmode
+ default:
+ return BadRequest(err.Error())
+ }
+ }
+
+ if snapName == "" {
+ // potentially dangerous but dangerous or devmode params were set
+ info, err := unsafeReadSnapInfo(tempPath)
+ if err != nil {
+ return BadRequest("cannot read snap file: %v", err)
+ }
+ snapName = info.Name()
+ sideInfo = &snap.SideInfo{RealName: snapName}
+ }
+
+ msg := fmt.Sprintf(i18n.G("Install %q snap from file"), snapName)
+ if origPath != "" {
+ msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), snapName, origPath)
+ }
+
+ var userID int
+ if user != nil {
+ userID = user.ID
+ }
+
+ tsets, err := withEnsureUbuntuCore(st, snapName, userID,
+ func() (*state.TaskSet, error) {
+ return snapstateInstallPath(st, sideInfo, tempPath, "", flags)
+ },
+ )
+ if err != nil {
+ return InternalError("cannot install snap file: %v", err)
+ }
+
+ chg := newChange(st, "install-snap", msg, tsets, []string{snapName})
+ chg.Set("api-data", map[string]string{"snap-name": snapName})
+
+ ensureStateSoon(st)
+
+ // only when the unlock succeeds (as opposed to panicing) is the handoff done
+ // but this is good enough
+ changeTriggered = true
+
+ return AsyncResponse(nil, &Meta{Change: chg.ID()})
+}
+
+func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) {
+ // Condider using DeriveSideInfo before falling back to this!
+ snapf, err := snap.Open(snapPath)
+ if err != nil {
+ return nil, err
+ }
+ return snap.ReadInfoFromSnapFile(snapf, nil)
+}
+
+var unsafeReadSnapInfo = unsafeReadSnapInfoImpl
+
+func iconGet(st *state.State, name string) Response {
+ about, err := localSnapInfo(st, name)
+ if err != nil {
+ if err == errNoSnap {
+ return NotFound("cannot find snap %q", name)
+ }
+ return InternalError("%v", err)
+ }
+
+ path := filepath.Clean(snapIcon(about.info))
+ if !strings.HasPrefix(path, dirs.SnapMountDir) {
+ // XXX: how could this happen?
+ return BadRequest("requested icon is not in snap path")
+ }
+
+ return FileResponse(path)
+}
+
+func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response {
+ vars := muxVars(r)
+ name := vars["name"]
+
+ return iconGet(c.d.overlord.State(), name)
+}
+
+func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response {
+ vars := muxVars(r)
+ snapName := vars["name"]
+
+ keys := strings.Split(r.URL.Query().Get("keys"), ",")
+ if len(keys) == 0 {
+ return BadRequest("cannot obtain configuration: no keys supplied")
+ }
+
+ s := c.d.overlord.State()
+ s.Lock()
+ transaction := configstate.NewTransaction(s)
+ s.Unlock()
+
+ currentConfValues := make(map[string]interface{})
+ for _, key := range keys {
+ var value interface{}
+ if err := transaction.Get(snapName, key, &value); err != nil {
+ return BadRequest("%s", err)
+ }
+
+ currentConfValues[key] = value
+ }
+
+ return SyncResponse(currentConfValues, nil)
+}
+
+func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response {
+ vars := muxVars(r)
+ snapName := vars["name"]
+
+ var patchValues map[string]interface{}
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&patchValues); err != nil {
+ return BadRequest("cannot decode request body into patch values: %v", err)
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ if err := snapstate.Get(st, snapName, &snapst); err == state.ErrNoState {
+ return NotFound("cannot find %q snap", snapName)
+ } else if err != nil {
+ return InternalError("%v", err)
+ }
+
+ taskset := configstate.Configure(st, snapName, patchValues)
+
+ summary := fmt.Sprintf("Change configuration of %q snap", snapName)
+ change := newChange(st, "configure-snap", summary, []*state.TaskSet{taskset}, []string{snapName})
+
+ st.EnsureBefore(0)
+
+ return AsyncResponse(nil, &Meta{Change: change.ID()})
+}
+
+// getInterfaces returns all plugs and slots.
+func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
+ repo := c.d.overlord.InterfaceManager().Repository()
+ return SyncResponse(repo.Interfaces(), nil)
+}
+
+// plugJSON aids in marshaling Plug into JSON.
+type plugJSON struct {
+ Snap string `json:"snap"`
+ Name string `json:"plug"`
+ Interface string `json:"interface"`
+ Attrs map[string]interface{} `json:"attrs,omitempty"`
+ Apps []string `json:"apps,omitempty"`
+ Label string `json:"label"`
+ Connections []interfaces.SlotRef `json:"connections,omitempty"`
+}
+
+// slotJSON aids in marshaling Slot into JSON.
+type slotJSON struct {
+ Snap string `json:"snap"`
+ Name string `json:"slot"`
+ Interface string `json:"interface"`
+ Attrs map[string]interface{} `json:"attrs,omitempty"`
+ Apps []string `json:"apps,omitempty"`
+ Label string `json:"label"`
+ Connections []interfaces.PlugRef `json:"connections,omitempty"`
+}
+
+// interfaceAction is an action performed on the interface system.
+type interfaceAction struct {
+ Action string `json:"action"`
+ Plugs []plugJSON `json:"plugs,omitempty"`
+ Slots []slotJSON `json:"slots,omitempty"`
+}
+
+// changeInterfaces controls the interfaces system.
+// Plugs can be connected to and disconnected from slots.
+// When enableInternalInterfaceActions is true plugs and slots can also be
+// explicitly added and removed.
+func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response {
+ var a interfaceAction
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&a); err != nil {
+ return BadRequest("cannot decode request body into an interface action: %v", err)
+ }
+ if a.Action == "" {
+ return BadRequest("interface action not specified")
+ }
+ if !c.d.enableInternalInterfaceActions && a.Action != "connect" && a.Action != "disconnect" {
+ return BadRequest("internal interface actions are disabled")
+ }
+ if len(a.Plugs) > 1 || len(a.Slots) > 1 {
+ return NotImplemented("many-to-many operations are not implemented")
+ }
+ if a.Action != "connect" && a.Action != "disconnect" {
+ return BadRequest("unsupported interface action: %q", a.Action)
+ }
+ if len(a.Plugs) == 0 || len(a.Slots) == 0 {
+ return BadRequest("at least one plug and slot is required")
+ }
+
+ var summary string
+ var taskset *state.TaskSet
+ var err error
+
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ switch a.Action {
+ case "connect":
+ var connRef interfaces.ConnRef
+ repo := c.d.overlord.InterfaceManager().Repository()
+ connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
+ if err == nil {
+ summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
+ taskset, err = ifacestate.Connect(state, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name)
+ }
+ case "disconnect":
+ summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
+ taskset, err = ifacestate.Disconnect(state, a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name)
+ }
+ if err != nil {
+ return BadRequest("%v", err)
+ }
+
+ change := state.NewChange(a.Action+"-snap", summary)
+ change.Set("snap-names", []string{a.Plugs[0].Snap, a.Slots[0].Snap})
+ change.AddAll(taskset)
+
+ state.EnsureBefore(0)
+
+ return AsyncResponse(nil, &Meta{Change: change.ID()})
+}
+
+func doAssert(c *Command, r *http.Request, user *auth.UserState) Response {
+ batch := assertstate.NewBatch()
+ _, err := batch.AddStream(r.Body)
+ if err != nil {
+ return BadRequest("cannot decode request body into assertions: %v", err)
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ if err := batch.Commit(state); err != nil {
+ return BadRequest("assert failed: %v", err)
+ }
+ // TODO: what more info do we want to return on success?
+ return &resp{
+ Type: ResponseTypeSync,
+ Status: http.StatusOK,
+ }
+}
+
+func assertsFindMany(c *Command, r *http.Request, user *auth.UserState) Response {
+ assertTypeName := muxVars(r)["assertType"]
+ assertType := asserts.Type(assertTypeName)
+ if assertType == nil {
+ return BadRequest("invalid assert type: %q", assertTypeName)
+ }
+ headers := map[string]string{}
+ q := r.URL.Query()
+ for k := range q {
+ headers[k] = q.Get(k)
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ db := assertstate.DB(state)
+ state.Unlock()
+
+ assertions, err := db.FindMany(assertType, headers)
+ if err == asserts.ErrNotFound {
+ return AssertResponse(nil, true)
+ } else if err != nil {
+ return InternalError("searching assertions failed: %v", err)
+ }
+ return AssertResponse(assertions, true)
+}
+
+type changeInfo struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Status string `json:"status"`
+ Tasks []*taskInfo `json:"tasks,omitempty"`
+ Ready bool `json:"ready"`
+ Err string `json:"err,omitempty"`
+
+ SpawnTime time.Time `json:"spawn-time,omitempty"`
+ ReadyTime *time.Time `json:"ready-time,omitempty"`
+
+ Data map[string]*json.RawMessage `json:"data,omitempty"`
+}
+
+type taskInfo struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Status string `json:"status"`
+ Log []string `json:"log,omitempty"`
+ Progress taskInfoProgress `json:"progress"`
+
+ SpawnTime time.Time `json:"spawn-time,omitempty"`
+ ReadyTime *time.Time `json:"ready-time,omitempty"`
+}
+
+type taskInfoProgress struct {
+ Label string `json:"label"`
+ Done int `json:"done"`
+ Total int `json:"total"`
+}
+
+func change2changeInfo(chg *state.Change) *changeInfo {
+ status := chg.Status()
+ chgInfo := &changeInfo{
+ ID: chg.ID(),
+ Kind: chg.Kind(),
+ Summary: chg.Summary(),
+ Status: status.String(),
+ Ready: status.Ready(),
+
+ SpawnTime: chg.SpawnTime(),
+ }
+ readyTime := chg.ReadyTime()
+ if !readyTime.IsZero() {
+ chgInfo.ReadyTime = &readyTime
+ }
+ if err := chg.Err(); err != nil {
+ chgInfo.Err = err.Error()
+ }
+
+ tasks := chg.Tasks()
+ taskInfos := make([]*taskInfo, len(tasks))
+ for j, t := range tasks {
+ label, done, total := t.Progress()
+
+ taskInfo := &taskInfo{
+ ID: t.ID(),
+ Kind: t.Kind(),
+ Summary: t.Summary(),
+ Status: t.Status().String(),
+ Log: t.Log(),
+ Progress: taskInfoProgress{
+ Label: label,
+ Done: done,
+ Total: total,
+ },
+ SpawnTime: t.SpawnTime(),
+ }
+ readyTime := t.ReadyTime()
+ if !readyTime.IsZero() {
+ taskInfo.ReadyTime = &readyTime
+ }
+ taskInfos[j] = taskInfo
+ }
+ chgInfo.Tasks = taskInfos
+
+ var data map[string]*json.RawMessage
+ if chg.Get("api-data", &data) == nil {
+ chgInfo.Data = data
+ }
+
+ return chgInfo
+}
+
+func getChange(c *Command, r *http.Request, user *auth.UserState) Response {
+ chID := muxVars(r)["id"]
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+ chg := state.Change(chID)
+ if chg == nil {
+ return NotFound("cannot find change with id %q", chID)
+ }
+
+ return SyncResponse(change2changeInfo(chg), nil)
+}
+
+func getChanges(c *Command, r *http.Request, user *auth.UserState) Response {
+ query := r.URL.Query()
+ qselect := query.Get("select")
+ if qselect == "" {
+ qselect = "in-progress"
+ }
+ var filter func(*state.Change) bool
+ switch qselect {
+ case "all":
+ filter = func(*state.Change) bool { return true }
+ case "in-progress":
+ filter = func(chg *state.Change) bool { return !chg.Status().Ready() }
+ case "ready":
+ filter = func(chg *state.Change) bool { return chg.Status().Ready() }
+ default:
+ return BadRequest("select should be one of: all,in-progress,ready")
+ }
+
+ if wantedName := query.Get("for"); wantedName != "" {
+ outerFilter := filter
+ filter = func(chg *state.Change) bool {
+ if !outerFilter(chg) {
+ return false
+ }
+
+ var snapNames []string
+ if err := chg.Get("snap-names", &snapNames); err != nil {
+ logger.Noticef("Cannot get snap-name for change %v", chg.ID())
+ return false
+ }
+
+ for _, snapName := range snapNames {
+ if snapName == wantedName {
+ return true
+ }
+ }
+
+ return false
+ }
+ }
+
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+ chgs := state.Changes()
+ chgInfos := make([]*changeInfo, 0, len(chgs))
+ for _, chg := range chgs {
+ if !filter(chg) {
+ continue
+ }
+ chgInfos = append(chgInfos, change2changeInfo(chg))
+ }
+ return SyncResponse(chgInfos, nil)
+}
+
+func abortChange(c *Command, r *http.Request, user *auth.UserState) Response {
+ chID := muxVars(r)["id"]
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+ chg := state.Change(chID)
+ if chg == nil {
+ return NotFound("cannot find change with id %q", chID)
+ }
+
+ var reqData struct {
+ Action string `json:"action"`
+ }
+
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&reqData); err != nil {
+ return BadRequest("cannot decode data from request body: %v", err)
+ }
+
+ if reqData.Action != "abort" {
+ return BadRequest("change action %q is unsupported", reqData.Action)
+ }
+
+ if chg.Status().Ready() {
+ return BadRequest("cannot abort change %s with nothing pending", chID)
+ }
+
+ // flag the change
+ chg.Abort()
+
+ // actually ask to proceed with the abort
+ ensureStateSoon(state)
+
+ return SyncResponse(change2changeInfo(chg), nil)
+}
+
+var (
+ postCreateUserUcrednetGetUID = ucrednetGetUID
+ storeUserInfo = store.UserInfo
+ osutilAddUser = osutil.AddUser
+)
+
+func getUserDetailsFromStore(email string) (string, *osutil.AddUserOptions, error) {
+ v, err := storeUserInfo(email)
+ if err != nil {
+ return "", nil, fmt.Errorf("cannot create user %q: %s", email, err)
+ }
+ if len(v.SSHKeys) == 0 {
+ return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email)
+ }
+
+ gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier)
+ opts := &osutil.AddUserOptions{
+ SSHKeys: v.SSHKeys,
+ Gecos: gecos,
+ }
+ return v.Username, opts, nil
+}
+
+func createAllKnownSystemUsers(st *state.State, createData *postUserCreateData) Response {
+ var createdUsers []userResponseData
+
+ st.Lock()
+ db := assertstate.DB(st)
+ modelAs, err := devicestate.Model(st)
+ st.Unlock()
+ if err != nil {
+ return InternalError("cannot get model assertion")
+ }
+
+ headers := map[string]string{
+ "brand-id": modelAs.BrandID(),
+ }
+ st.Lock()
+ assertions, err := db.FindMany(asserts.SystemUserType, headers)
+ st.Unlock()
+ if err != nil && err != asserts.ErrNotFound {
+ return BadRequest("cannot find system-user assertion: %s", err)
+ }
+
+ for _, as := range assertions {
+ email := as.(*asserts.SystemUser).Email()
+ // we need to use getUserDetailsFromAssertion as this verifies
+ // the assertion against the current brand/model/time
+ username, opts, err := getUserDetailsFromAssertion(st, email)
+ if err != nil {
+ logger.Noticef("ignoring system-user assertion for %q: %s", email, err)
+ continue
+ }
+ // ignore already existing users
+ if _, err := user.Lookup(username); err == nil {
+ continue
+ }
+
+ // FIXME: duplicated code
+ opts.Sudoer = createData.Sudoer
+ opts.ExtraUsers = !release.OnClassic
+
+ if err := osutilAddUser(username, opts); err != nil {
+ return InternalError("cannot add user %q: %s", username, err)
+ }
+ if err := setupLocalUser(st, username, email); err != nil {
+ return InternalError("%s", err)
+ }
+ createdUsers = append(createdUsers, userResponseData{
+ Username: username,
+ SSHKeys: opts.SSHKeys,
+ })
+ }
+
+ return SyncResponse(createdUsers, nil)
+}
+
+func getUserDetailsFromAssertion(st *state.State, email string) (string, *osutil.AddUserOptions, error) {
+ errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email)
+
+ st.Lock()
+ db := assertstate.DB(st)
+ modelAs, err := devicestate.Model(st)
+ st.Unlock()
+ if err != nil {
+ return "", nil, fmt.Errorf(errorPrefix+"cannot get model assertion: %s", err)
+ }
+
+ brandID := modelAs.BrandID()
+ series := modelAs.Series()
+ model := modelAs.Model()
+
+ a, err := db.Find(asserts.SystemUserType, map[string]string{
+ "brand-id": brandID,
+ "email": email,
+ })
+ if err != nil {
+ return "", nil, fmt.Errorf(errorPrefix+"%v", err)
+ }
+ // the asserts package guarantees that this cast will work
+ su := a.(*asserts.SystemUser)
+
+ // cross check that the assertion is valid for the given series/model
+ contains := func(needle string, haystack []string) bool {
+ for _, s := range haystack {
+ if needle == s {
+ return true
+ }
+ }
+ return false
+ }
+ // check that the signer of the assertion is one of the accepted ones
+ sysUserAuths := modelAs.SystemUserAuthority()
+ if len(sysUserAuths) > 0 && !contains(su.AuthorityID(), sysUserAuths) {
+ return "", nil, fmt.Errorf(errorPrefix+"%q not in accepted authorities %q", email, su.AuthorityID(), sysUserAuths)
+ }
+ if len(su.Series()) > 0 && !contains(series, su.Series()) {
+ return "", nil, fmt.Errorf(errorPrefix+"%q not in series %q", email, series, su.Series())
+ }
+ if len(su.Models()) > 0 && !contains(model, su.Models()) {
+ return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models())
+ }
+ if !su.ValidAt(time.Now()) {
+ return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore")
+ }
+
+ gecos := fmt.Sprintf("%s,%s", email, su.Name())
+ opts := &osutil.AddUserOptions{
+ SSHKeys: su.SSHKeys(),
+ Gecos: gecos,
+ Password: su.Password(),
+ }
+ return su.Username(), opts, nil
+}
+
+type postUserCreateData struct {
+ Email string `json:"email"`
+ Sudoer bool `json:"sudoer"`
+ Known bool `json:"known"`
+ ForceManaged bool `json:"force-managed"`
+}
+
+var userLookup = user.Lookup
+
+func setupLocalUser(st *state.State, username, email string) error {
+ user, err := userLookup(username)
+ if err != nil {
+ return fmt.Errorf("cannot lookup user %q: %s", username, err)
+ }
+ uid, err := strconv.Atoi(user.Uid)
+ if err != nil {
+ return fmt.Errorf("cannot get uid of user %q: %s", username, err)
+ }
+ gid, err := strconv.Atoi(user.Gid)
+ if err != nil {
+ return fmt.Errorf("cannot get gid of user %q: %s", username, err)
+ }
+ authDataFn := filepath.Join(user.HomeDir, ".snap", "auth.json")
+ if err := osutil.MkdirAllChown(filepath.Dir(authDataFn), 0700, uid, gid); err != nil {
+ return err
+ }
+
+ // setup new user, local-only
+ st.Lock()
+ authUser, err := auth.NewUser(st, username, email, "", nil)
+ st.Unlock()
+ if err != nil {
+ return fmt.Errorf("cannot persist authentication details: %v", err)
+ }
+ // store macaroon auth in auth.json in the new users home dir
+ outStr, err := json.Marshal(struct {
+ Macaroon string `json:"macaroon"`
+ }{
+ Macaroon: authUser.Macaroon,
+ })
+ if err != nil {
+ return fmt.Errorf("cannot marshal auth data: %s", err)
+ }
+ if err := osutil.AtomicWriteFileChown(authDataFn, []byte(outStr), 0600, 0, uid, gid); err != nil {
+ return fmt.Errorf("cannot write auth file %q: %s", authDataFn, err)
+ }
+
+ return nil
+}
+
+func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response {
+ uid, err := postCreateUserUcrednetGetUID(r.RemoteAddr)
+ if err != nil {
+ return BadRequest("cannot get ucrednet uid: %v", err)
+ }
+ if uid != 0 {
+ return BadRequest("cannot use create-user as non-root")
+ }
+
+ var createData postUserCreateData
+
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&createData); err != nil {
+ return BadRequest("cannot decode create-user data from request body: %v", err)
+ }
+
+ // verify request
+ st := c.d.overlord.State()
+ st.Lock()
+ users, err := auth.Users(st)
+ st.Unlock()
+ if err != nil {
+ return InternalError("cannot get user count: %s", err)
+ }
+
+ if !createData.ForceManaged {
+ if len(users) > 0 {
+ return BadRequest("cannot create user: device already managed")
+ }
+ if release.OnClassic {
+ return BadRequest("cannot create user: device is a classic system")
+ }
+ }
+
+ // special case: the user requested the creation of all known
+ // system-users
+ if createData.Email == "" && createData.Known {
+ return createAllKnownSystemUsers(c.d.overlord.State(), &createData)
+ }
+ if createData.Email == "" {
+ return BadRequest("cannot create user: 'email' field is empty")
+ }
+
+ var username string
+ var opts *osutil.AddUserOptions
+ if createData.Known {
+ username, opts, err = getUserDetailsFromAssertion(st, createData.Email)
+ } else {
+ username, opts, err = getUserDetailsFromStore(createData.Email)
+ }
+ if err != nil {
+ return BadRequest("%s", err)
+ }
+
+ // FIXME: duplicated code
+ opts.Sudoer = createData.Sudoer
+ opts.ExtraUsers = !release.OnClassic
+
+ if err := osutilAddUser(username, opts); err != nil {
+ return BadRequest("cannot create user %s: %s", username, err)
+ }
+
+ if err := setupLocalUser(c.d.overlord.State(), username, createData.Email); err != nil {
+ return InternalError("%s", err)
+ }
+
+ return SyncResponse(&userResponseData{
+ Username: username,
+ SSHKeys: opts.SSHKeys,
+ }, nil)
+}
+
+func convertBuyError(err error) Response {
+ switch err {
+ case nil:
+ return nil
+ case store.ErrInvalidCredentials:
+ return Unauthorized(err.Error())
+ case store.ErrUnauthenticated:
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindLoginRequired,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ case store.ErrTOSNotAccepted:
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindTermsNotAccepted,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ case store.ErrNoPaymentMethods:
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindNoPaymentMethods,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ case store.ErrPaymentDeclined:
+ return SyncResponse(&resp{
+ Type: ResponseTypeError,
+ Result: &errorResult{
+ Message: err.Error(),
+ Kind: errorKindPaymentDeclined,
+ },
+ Status: http.StatusBadRequest,
+ }, nil)
+ default:
+ return InternalError("%v", err)
+ }
+}
+
+func postBuy(c *Command, r *http.Request, user *auth.UserState) Response {
+ var opts store.BuyOptions
+
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&opts)
+ if err != nil {
+ return BadRequest("cannot decode buy options from request body: %v", err)
+ }
+
+ s := getStore(c)
+
+ buyResult, err := s.Buy(&opts, user)
+
+ if resp := convertBuyError(err); resp != nil {
+ return resp
+ }
+
+ return SyncResponse(buyResult, nil)
+}
+
+func readyToBuy(c *Command, r *http.Request, user *auth.UserState) Response {
+ s := getStore(c)
+
+ if resp := convertBuyError(s.ReadyToBuy(user)); resp != nil {
+ return resp
+ }
+
+ return SyncResponse(true, nil)
+}
+
+func runSnapctl(c *Command, r *http.Request, user *auth.UserState) Response {
+ var snapctlOptions client.SnapCtlOptions
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&snapctlOptions); err != nil {
+ return BadRequest("cannot decode snapctl request: %s", err)
+ }
+
+ if len(snapctlOptions.Args) == 0 {
+ return BadRequest("snapctl cannot run without args")
+ }
+
+ // Right now snapctl is only used for hooks. If at some point it grows
+ // beyond that, this probably shouldn't go straight to the HookManager.
+ context, _ := c.d.overlord.HookManager().Context(snapctlOptions.ContextID)
+ stdout, stderr, err := ctlcmd.Run(context, snapctlOptions.Args)
+ if err != nil {
+ if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
+ stdout = []byte(e.Error())
+ } else {
+ return BadRequest("error running snapctl: %s", err)
+ }
+ }
+
+ result := map[string]string{
+ "stdout": string(stdout),
+ "stderr": string(stderr),
+ }
+
+ return SyncResponse(result, nil)
+}
+
+func getUsers(c *Command, r *http.Request, user *auth.UserState) Response {
+ uid, err := postCreateUserUcrednetGetUID(r.RemoteAddr)
+ if err != nil {
+ return BadRequest("cannot get ucrednet uid: %v", err)
+ }
+ if uid != 0 {
+ return BadRequest("cannot use create-user as non-root")
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ users, err := auth.Users(st)
+ st.Unlock()
+ if err != nil {
+ return InternalError("cannot get users: %s", err)
+ }
+
+ resp := make([]userResponseData, len(users))
+ for i, u := range users {
+ resp[i] = userResponseData{
+ Username: u.Username,
+ Email: u.Email,
+ ID: u.ID,
+ }
+ }
+ return SyncResponse(resp, nil)
+}
+
+// aliasAction is an action performed on aliases
+type aliasAction struct {
+ Action string `json:"action"`
+ Snap string `json:"snap"`
+ Aliases []string `json:"aliases"`
+}
+
+func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response {
+ var a aliasAction
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&a); err != nil {
+ return BadRequest("cannot decode request body into an alias action: %v", err)
+ }
+ if len(a.Aliases) == 0 {
+ return BadRequest("at least one alias name is required")
+ }
+
+ var summary string
+ var taskset *state.TaskSet
+ var err error
+
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ switch a.Action {
+ default:
+ return BadRequest("unsupported alias action: %q", a.Action)
+ case "alias":
+ summary = fmt.Sprintf("Enable aliases %s for snap %q", strutil.Quoted(a.Aliases), a.Snap)
+ taskset, err = snapstate.Alias(state, a.Snap, a.Aliases)
+ case "unalias":
+ summary = fmt.Sprintf("Disable aliases %s for snap %q", strutil.Quoted(a.Aliases), a.Snap)
+ taskset, err = snapstate.Unalias(state, a.Snap, a.Aliases)
+ case "reset":
+ summary = fmt.Sprintf("Reset aliases %s for snap %q", strutil.Quoted(a.Aliases), a.Snap)
+ taskset, err = snapstate.ResetAliases(state, a.Snap, a.Aliases)
+ }
+ if err != nil {
+ return BadRequest("%v", err)
+ }
+
+ change := state.NewChange(a.Action, summary)
+ change.Set("snap-names", []string{a.Snap})
+ change.AddAll(taskset)
+
+ state.EnsureBefore(0)
+
+ return AsyncResponse(nil, &Meta{Change: change.ID()})
+}
+
+type aliasStatus struct {
+ App string `json:"app,omitempty"`
+ Status string `json:"status,omitempty"`
+}
+
+// getAliases produces a response with a map snap -> alias -> aliasStatus
+func getAliases(c *Command, r *http.Request, user *auth.UserState) Response {
+ state := c.d.overlord.State()
+ state.Lock()
+ defer state.Unlock()
+
+ res := make(map[string]map[string]aliasStatus)
+
+ allStates, err := snapstate.All(state)
+ if err != nil {
+ return InternalError("cannot list local snaps: %v", err)
+ }
+
+ allAliases, err := snapstate.Aliases(state)
+ if err != nil {
+ return InternalError("cannot list aliases: %v", err)
+ }
+
+ for snapName, snapst := range allStates {
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return InternalError("cannot retrieve info for snap %q: %v", snapName, err)
+ }
+ if len(info.Aliases) != 0 {
+ snapAliases := make(map[string]aliasStatus)
+ res[snapName] = snapAliases
+ for alias, aliasApp := range info.Aliases {
+ snapAliases[alias] = aliasStatus{
+ App: filepath.Base(aliasApp.WrapperPath()),
+ }
+ }
+ }
+ }
+
+ for snapName, aliasStatuses := range allAliases {
+ snapAliases := res[snapName]
+ if snapAliases == nil {
+ snapAliases = make(map[string]aliasStatus)
+ res[snapName] = snapAliases
+ }
+ for alias, status := range aliasStatuses {
+ entry := snapAliases[alias]
+ entry.Status = status
+ snapAliases[alias] = entry
+ }
+ }
+
+ return SyncResponse(res, nil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+func (s *apiSuite) mockSnap(c *C, yamlText string) *snap.Info {
+ if s.d == nil {
+ panic("call s.daemon(c) in your test first")
+ }
+
+ snapInfo := snaptest.MockSnap(c, yamlText, "", &snap.SideInfo{Revision: snap.R(1)})
+ snap.AddImplicitSlots(snapInfo)
+
+ st := s.d.overlord.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ // Put a side info into the state
+ snapstate.Set(st, snapInfo.Name(), &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ SnapID: "ididid",
+ },
+ },
+ Current: snapInfo.Revision,
+ })
+
+ // Put the snap into the interface repository
+ repo := s.d.overlord.InterfaceManager().Repository()
+ err := repo.AddSnap(snapInfo)
+ c.Assert(err, IsNil)
+ return snapInfo
+}
+
+func (s *apiSuite) mockIface(c *C, iface interfaces.Interface) {
+ if s.d == nil {
+ panic("call s.daemon(c) in your test first")
+ }
+ err := s.d.overlord.InterfaceManager().Repository().AddInterface(iface)
+ c.Assert(err, IsNil)
+}
+
+var simpleYaml = `
+name: simple
+version: 1
+`
+
+var consumerYaml = `
+name: consumer
+version: 1
+apps:
+ app:
+plugs:
+ plug:
+ interface: test
+ key: value
+ label: label
+`
+
+var producerYaml = `
+name: producer
+version: 1
+apps:
+ app:
+slots:
+ slot:
+ interface: test
+ key: value
+ label: label
+`
+
+var differentProducerYaml = `
+name: producer
+version: 1
+apps:
+ app:
+slots:
+ slot:
+ interface: different
+ key: value
+ label: label
+`
+
+var configYaml = `
+name: config-snap
+version: 1
+hooks:
+ configure:
+`
+var aliasYaml = `
+name: alias-snap
+version: 1
+apps:
+ app:
+ aliases: [alias1]
+ app2:
+ aliases: [alias2]
+`
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "bytes"
+ "crypto"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/user"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/sha3"
+ "golang.org/x/net/context"
+ "gopkg.in/check.v1"
+ "gopkg.in/macaroon.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/ifacestate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type apiBaseSuite struct {
+ rsnaps []*snap.Info
+ err error
+ vars map[string]string
+ storeSearch store.Search
+ suggestedCurrency string
+ d *Daemon
+ user *auth.UserState
+ restoreBackends func()
+ refreshCandidates []*store.RefreshCandidate
+ buyOptions *store.BuyOptions
+ buyResult *store.BuyResult
+ storeSigning *assertstest.StoreStack
+ restoreRelease func()
+ trustedRestorer func()
+}
+
+func (s *apiBaseSuite) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
+ s.user = user
+ if len(s.rsnaps) > 0 {
+ return s.rsnaps[0], s.err
+ }
+ return nil, s.err
+}
+
+func (s *apiBaseSuite) Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error) {
+ s.storeSearch = *search
+ s.user = user
+
+ return s.rsnaps, s.err
+}
+
+func (s *apiBaseSuite) ListRefresh(snaps []*store.RefreshCandidate, user *auth.UserState) ([]*snap.Info, error) {
+ s.refreshCandidates = snaps
+ s.user = user
+
+ return s.rsnaps, s.err
+}
+
+func (s *apiBaseSuite) SuggestedCurrency() string {
+ return s.suggestedCurrency
+}
+
+func (s *apiBaseSuite) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error {
+ panic("Download not expected to be called")
+}
+
+func (s *apiBaseSuite) Buy(options *store.BuyOptions, user *auth.UserState) (*store.BuyResult, error) {
+ s.buyOptions = options
+ s.user = user
+ return s.buyResult, s.err
+}
+
+func (s *apiBaseSuite) ReadyToBuy(user *auth.UserState) error {
+ s.user = user
+ return s.err
+}
+
+func (s *apiBaseSuite) Assertion(*asserts.AssertionType, []string, *auth.UserState) (asserts.Assertion, error) {
+ panic("Assertion not expected to be called")
+}
+
+func (s *apiBaseSuite) Sections(*auth.UserState) ([]string, error) {
+ panic("Sections not expected to be called")
+}
+
+func (s *apiBaseSuite) muxVars(*http.Request) map[string]string {
+ return s.vars
+}
+
+func (s *apiBaseSuite) SetUpSuite(c *check.C) {
+ muxVars = s.muxVars
+ s.restoreRelease = release.MockReleaseInfo(&release.OS{
+ ID: "ubuntu",
+ VersionID: "mocked",
+ })
+}
+
+func (s *apiBaseSuite) TearDownSuite(c *check.C) {
+ muxVars = nil
+ s.restoreRelease()
+}
+
+var (
+ rootPrivKey, _ = assertstest.GenerateKey(1024)
+ storePrivKey, _ = assertstest.GenerateKey(752)
+)
+
+func (s *apiBaseSuite) SetUpTest(c *check.C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, check.IsNil)
+ c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil)
+
+ s.rsnaps = nil
+ s.suggestedCurrency = ""
+ s.storeSearch = store.Search{}
+ s.err = nil
+ s.vars = nil
+ s.user = nil
+ s.d = nil
+ s.refreshCandidates = nil
+ // Disable real security backends for all API tests
+ s.restoreBackends = ifacestate.MockSecurityBackends(nil)
+
+ s.buyOptions = nil
+ s.buyResult = nil
+
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+ s.trustedRestorer = sysdb.InjectTrusted(s.storeSigning.Trusted)
+}
+
+func (s *apiBaseSuite) TearDownTest(c *check.C) {
+ s.trustedRestorer()
+ s.d = nil
+ s.restoreBackends()
+ snapstateInstall = snapstate.Install
+ snapstateCoreInfo = snapstate.CoreInfo
+ snapstateInstallPath = snapstate.InstallPath
+ assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations
+ unsafeReadSnapInfo = unsafeReadSnapInfoImpl
+ ensureStateSoon = ensureStateSoonImpl
+ dirs.SetRootDir("")
+}
+
+func (s *apiBaseSuite) daemon(c *check.C) *Daemon {
+ if s.d != nil {
+ panic("called daemon() twice")
+ }
+ d, err := New()
+ c.Assert(err, check.IsNil)
+ d.addRoutes()
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ snapstate.ReplaceStore(st, s)
+
+ s.d = d
+ return d
+}
+
+func (s *apiBaseSuite) mkInstalled(c *check.C, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info {
+ return s.mkInstalledInState(c, nil, name, developer, version, revision, active, extraYaml)
+}
+
+func (s *apiBaseSuite) mkInstalledInState(c *check.C, daemon *Daemon, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info {
+ snapID := name + "-id"
+ // Collect arguments into a snap.SideInfo structure
+ sideInfo := &snap.SideInfo{
+ SnapID: snapID,
+ RealName: name,
+ Revision: revision,
+ Channel: "stable",
+ }
+
+ // Collect other arguments into a yaml string
+ yamlText := fmt.Sprintf(`
+name: %s
+version: %s
+%s`, name, version, extraYaml)
+ contents := ""
+
+ // Mock the snap on disk
+ snapInfo := snaptest.MockSnap(c, yamlText, contents, sideInfo)
+
+ c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil)
+ metadir := filepath.Join(snapInfo.MountDir(), "meta")
+ guidir := filepath.Join(metadir, "gui")
+ c.Assert(os.MkdirAll(guidir, 0755), check.IsNil)
+ c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil)
+
+ if daemon != nil {
+ st := daemon.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ err := assertstate.Add(st, s.storeSigning.StoreAccountKey(""))
+ if _, ok := err.(*asserts.RevisionError); !ok {
+ c.Assert(err, check.IsNil)
+ }
+
+ devAcct := assertstest.NewAccount(s.storeSigning, developer, map[string]interface{}{
+ "account-id": developer + "-id",
+ }, "")
+ err = assertstate.Add(st, devAcct)
+ if _, ok := err.(*asserts.RevisionError); !ok {
+ c.Assert(err, check.IsNil)
+ }
+
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": snapID,
+ "snap-name": name,
+ "publisher-id": devAcct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ err = assertstate.Add(st, snapDecl)
+ if _, ok := err.(*asserts.RevisionError); !ok {
+ c.Assert(err, check.IsNil)
+ }
+
+ h := sha3.Sum384([]byte(fmt.Sprintf("%s%s", name, revision)))
+ dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:])
+ c.Assert(err, check.IsNil)
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": string(dgst),
+ "snap-size": "999",
+ "snap-id": snapID,
+ "snap-revision": fmt.Sprintf("%s", revision),
+ "developer-id": devAcct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ err = assertstate.Add(st, snapRev)
+ c.Assert(err, check.IsNil)
+
+ var snapst snapstate.SnapState
+ snapstate.Get(st, name, &snapst)
+ snapst.Active = active
+ snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo)
+ snapst.Current = snapInfo.SideInfo.Revision
+ snapst.Channel = "beta"
+
+ snapstate.Set(st, name, &snapst)
+ }
+
+ return snapInfo
+}
+
+func (s *apiBaseSuite) mkGadget(c *check.C, store string) {
+ yamlText := fmt.Sprintf(`name: test
+version: 1
+type: gadget
+gadget: {store: {id: %q}}
+`, store)
+ contents := ""
+ snaptest.MockSnap(c, yamlText, contents, &snap.SideInfo{Revision: snap.R(1)})
+ c.Assert(os.Symlink("1", filepath.Join(dirs.SnapMountDir, "test", "current")), check.IsNil)
+}
+
+type apiSuite struct {
+ apiBaseSuite
+}
+
+var _ = check.Suite(&apiSuite{})
+
+func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) {
+ d := s.daemon(c)
+ s.vars = map[string]string{"name": "foo"}
+
+ // we have v0 [r5] installed
+ s.mkInstalledInState(c, d, "foo", "bar", "v0", snap.R(5), false, "")
+ // and v1 [r10] is current
+ s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "description: description\nsummary: summary")
+
+ req, err := http.NewRequest("GET", "/v2/snaps/foo", nil)
+ c.Assert(err, check.IsNil)
+ rsp, ok := getSnapInfo(snapCmd, req, nil).(*resp)
+ c.Assert(ok, check.Equals, true)
+
+ c.Assert(rsp, check.NotNil)
+ c.Assert(rsp.Result, check.FitsTypeOf, map[string]interface{}{})
+ m := rsp.Result.(map[string]interface{})
+
+ // installed-size depends on vagaries of the filesystem, just check type
+ c.Check(m["installed-size"], check.FitsTypeOf, int64(0))
+ delete(m, "installed-size")
+ // ditto install-date
+ c.Check(m["install-date"], check.FitsTypeOf, time.Time{})
+ delete(m, "install-date")
+
+ meta := &Meta{}
+ expected := &resp{
+ Type: ResponseTypeSync,
+ Status: http.StatusOK,
+ Result: map[string]interface{}{
+ "id": "foo-id",
+ "name": "foo",
+ "revision": snap.R(10),
+ "version": "v1",
+ "channel": "stable",
+ "tracking-channel": "beta",
+ "summary": "summary",
+ "description": "description",
+ "developer": "bar",
+ "status": "active",
+ "icon": "/v2/icons/foo/icon",
+ "type": string(snap.TypeApp),
+ "resource": "/v2/snaps/foo",
+ "private": false,
+ "devmode": false,
+ "jailmode": false,
+ "confinement": snap.StrictConfinement,
+ "trymode": false,
+ "apps": []appJSON{},
+ "broken": "",
+ },
+ Meta: meta,
+ }
+
+ c.Check(rsp.Result, check.DeepEquals, expected.Result)
+}
+
+func (s *apiSuite) TestSnapInfoWithAuth(c *check.C) {
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ req, err := http.NewRequest("GET", "/v2/find/?q=name:gfoo", nil)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(s.user, check.IsNil)
+
+ _, ok := searchStore(findCmd, req, user).(*resp)
+ c.Assert(ok, check.Equals, true)
+ // ensure user was set
+ c.Assert(s.user, check.DeepEquals, user)
+}
+
+func (s *apiSuite) TestSnapInfoNotFound(c *check.C) {
+ req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil)
+ c.Assert(err, check.IsNil)
+ c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, http.StatusNotFound)
+}
+
+func (s *apiSuite) TestSnapInfoNoneFound(c *check.C) {
+ s.vars = map[string]string{"name": "foo"}
+
+ req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil)
+ c.Assert(err, check.IsNil)
+ c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, http.StatusNotFound)
+}
+
+func (s *apiSuite) TestSnapInfoIgnoresRemoteErrors(c *check.C) {
+ s.vars = map[string]string{"name": "foo"}
+ s.err = errors.New("weird")
+
+ req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getSnapInfo(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusNotFound)
+ c.Check(rsp.Result, check.NotNil)
+}
+
+func (s *apiSuite) TestListIncludesAll(c *check.C) {
+ // Very basic check to help stop us from not adding all the
+ // commands to the command list.
+ //
+ // It could get fancier, looking deeper into the AST to see
+ // exactly what's being defined, but it's probably not worth
+ // it; this gives us most of the benefits of that, with a
+ // fraction of the work.
+ //
+ // NOTE: there's probably a
+ // better/easier way of doing this (patches welcome)
+
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "api.go", nil, 0)
+ if err != nil {
+ panic(err)
+ }
+
+ found := 0
+
+ ast.Inspect(f, func(n ast.Node) bool {
+ switch v := n.(type) {
+ case *ast.ValueSpec:
+ found += len(v.Values)
+ return false
+ }
+ return true
+ })
+
+ exceptions := []string{ // keep sorted, for scanning ease
+ "isEmailish",
+ "api",
+ "maxReadBuflen",
+ "muxVars",
+ "errNothingToInstall",
+ "defaultCoreSnapName",
+ "oldDefaultSnapCoreName",
+ "errDevJailModeConflict",
+ "errNoJailMode",
+ "errClassicDevmodeConflict",
+ // snapInstruction vars:
+ "snapInstructionDispTable",
+ "snapstateInstall",
+ "snapstateUpdate",
+ "snapstateInstallPath",
+ "snapstateTryPath",
+ "snapstateCoreInfo",
+ "snapstateUpdateMany",
+ "snapstateInstallMany",
+ "snapstateRemoveMany",
+ "snapstateRefreshCandidates",
+ "assertstateRefreshSnapDeclarations",
+ "unsafeReadSnapInfo",
+ "osutilAddUser",
+ "setupLocalUser",
+ "storeUserInfo",
+ "postCreateUserUcrednetGetUID",
+ "ensureStateSoon",
+ }
+ c.Check(found, check.Equals, len(api)+len(exceptions),
+ check.Commentf(`At a glance it looks like you've not added all the Commands defined in api to the api list. If that is not the case, please add the exception to the "exceptions" list in this test.`))
+}
+
+func (s *apiSuite) TestRootCmd(c *check.C) {
+ // check it only does GET
+ c.Check(rootCmd.PUT, check.IsNil)
+ c.Check(rootCmd.POST, check.IsNil)
+ c.Check(rootCmd.DELETE, check.IsNil)
+ c.Assert(rootCmd.GET, check.NotNil)
+
+ rec := httptest.NewRecorder()
+ c.Check(rootCmd.Path, check.Equals, "/")
+
+ rootCmd.GET(rootCmd, nil, nil).ServeHTTP(rec, nil)
+ c.Check(rec.Code, check.Equals, 200)
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ expected := []interface{}{"TBD"}
+ var rsp resp
+ c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+}
+
+func (s *apiSuite) TestSysInfo(c *check.C) {
+ // check it only does GET
+ c.Check(sysInfoCmd.PUT, check.IsNil)
+ c.Check(sysInfoCmd.POST, check.IsNil)
+ c.Check(sysInfoCmd.DELETE, check.IsNil)
+ c.Assert(sysInfoCmd.GET, check.NotNil)
+
+ rec := httptest.NewRecorder()
+ c.Check(sysInfoCmd.Path, check.Equals, "/v2/system-info")
+
+ s.daemon(c).Version = "42b1"
+
+ restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"})
+ defer restore()
+ restore = release.MockOnClassic(true)
+ defer restore()
+ sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil)
+ c.Check(rec.Code, check.Equals, 200)
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
+
+ expected := map[string]interface{}{
+ "series": "16",
+ "version": "42b1",
+ "os-release": map[string]interface{}{
+ "id": "distro-id",
+ "version-id": "1.2",
+ },
+ "on-classic": true,
+ "managed": false,
+ }
+ var rsp resp
+ c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+}
+
+func (s *apiSuite) makeMyAppsServer(statusCode int, data string) *httptest.Server {
+ mockMyAppsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(statusCode)
+ io.WriteString(w, data)
+ }))
+ store.MyAppsMacaroonACLAPI = mockMyAppsServer.URL + "/acl/"
+ return mockMyAppsServer
+}
+
+func (s *apiSuite) makeSSOServer(statusCode int, data string) *httptest.Server {
+ mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(statusCode)
+ io.WriteString(w, data)
+ }))
+ store.UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge"
+ return mockSSOServer
+}
+
+func (s *apiSuite) makeStoreMacaroon() (string, error) {
+ m, err := macaroon.New([]byte("secret"), "some id", "location")
+ if err != nil {
+ return "", err
+ }
+ err = m.AddFirstPartyCaveat("caveat")
+ if err != nil {
+ return "", err
+ }
+ err = m.AddThirdPartyCaveat([]byte("shared-secret"), "third-party-caveat", store.UbuntuoneLocation)
+ if err != nil {
+ return "", err
+ }
+
+ return auth.MacaroonSerialize(m)
+}
+
+func (s *apiSuite) makeStoreMacaroonResponse(serializedMacaroon string) (string, error) {
+ data := map[string]string{
+ "macaroon": serializedMacaroon,
+ }
+ expectedData, err := json.Marshal(data)
+ if err != nil {
+ return "", err
+ }
+
+ return string(expectedData), nil
+}
+
+func (s *apiSuite) TestLoginUser(c *check.C) {
+ d := s.daemon(c)
+ state := d.overlord.State()
+
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}`
+ mockSSOServer := s.makeSSOServer(200, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(loginCmd, req, nil).(*resp)
+
+ state.Lock()
+ user, err := auth.User(state, 1)
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ expected := userResponseData{
+ ID: 1,
+ Email: "email@.com",
+
+ Macaroon: user.Macaroon,
+ Discharges: user.Discharges,
+ }
+
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Assert(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+
+ c.Check(user.ID, check.Equals, 1)
+ c.Check(user.Username, check.Equals, "")
+ c.Check(user.Email, check.Equals, "email@.com")
+ c.Check(user.Discharges, check.IsNil)
+ c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon)
+ c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
+ // snapd macaroon was setup too
+ snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon)
+ c.Check(err, check.IsNil)
+ c.Check(snapdMacaroon.Id(), check.Equals, "1")
+ c.Check(snapdMacaroon.Location(), check.Equals, "snapd")
+}
+
+func (s *apiSuite) TestLoginUserWithUsername(c *check.C) {
+ d := s.daemon(c)
+ state := d.overlord.State()
+
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}`
+ mockSSOServer := s.makeSSOServer(200, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "username", "email": "email@.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(loginCmd, req, nil).(*resp)
+
+ state.Lock()
+ user, err := auth.User(state, 1)
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ expected := userResponseData{
+ ID: 1,
+ Username: "username",
+ Email: "email@.com",
+ Macaroon: user.Macaroon,
+ Discharges: user.Discharges,
+ }
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Assert(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+
+ c.Check(user.ID, check.Equals, 1)
+ c.Check(user.Username, check.Equals, "username")
+ c.Check(user.Email, check.Equals, "email@.com")
+ c.Check(user.Discharges, check.IsNil)
+ c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon)
+ c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
+ // snapd macaroon was setup too
+ snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon)
+ c.Check(err, check.IsNil)
+ c.Check(snapdMacaroon.Id(), check.Equals, "1")
+ c.Check(snapdMacaroon.Location(), check.Equals, "snapd")
+}
+
+func (s *apiSuite) TestLoginUserNoEmailWithExistentLocalUser(c *check.C) {
+ d := s.daemon(c)
+ state := d.overlord.State()
+
+ // setup local-only user
+ state.Lock()
+ localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil)
+ state.Unlock()
+ c.Assert(err, check.IsNil)
+
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}`
+ mockSSOServer := s.makeSSOServer(200, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "username", "email": "", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon))
+
+ rsp := loginUser(loginCmd, req, localUser).(*resp)
+
+ expected := userResponseData{
+ ID: 1,
+ Username: "username",
+ Email: "email@test.com",
+
+ Macaroon: localUser.Macaroon,
+ Discharges: localUser.Discharges,
+ }
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Assert(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+
+ state.Lock()
+ user, err := auth.User(state, localUser.ID)
+ state.Unlock()
+ c.Check(err, check.IsNil)
+ c.Check(user.Username, check.Equals, "username")
+ c.Check(user.Email, check.Equals, localUser.Email)
+ c.Check(user.Macaroon, check.Equals, localUser.Macaroon)
+ c.Check(user.Discharges, check.IsNil)
+ c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon)
+ c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
+}
+
+func (s *apiSuite) TestLoginUserWithExistentLocalUser(c *check.C) {
+ d := s.daemon(c)
+ state := d.overlord.State()
+
+ // setup local-only user
+ state.Lock()
+ localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil)
+ state.Unlock()
+ c.Assert(err, check.IsNil)
+
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}`
+ mockSSOServer := s.makeSSOServer(200, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "username", "email": "email@test.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon))
+
+ rsp := loginUser(loginCmd, req, localUser).(*resp)
+
+ expected := userResponseData{
+ ID: 1,
+ Username: "username",
+ Email: "email@test.com",
+
+ Macaroon: localUser.Macaroon,
+ Discharges: localUser.Discharges,
+ }
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Assert(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+
+ state.Lock()
+ user, err := auth.User(state, localUser.ID)
+ state.Unlock()
+ c.Check(err, check.IsNil)
+ c.Check(user.Username, check.Equals, "username")
+ c.Check(user.Email, check.Equals, localUser.Email)
+ c.Check(user.Macaroon, check.Equals, localUser.Macaroon)
+ c.Check(user.Discharges, check.IsNil)
+ c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon)
+ c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"})
+}
+
+func (s *apiSuite) TestLogoutUser(c *check.C) {
+ d := s.daemon(c)
+ state := d.overlord.State()
+ state.Lock()
+ user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Assert(err, check.IsNil)
+
+ req, err := http.NewRequest("POST", "/v2/logout", nil)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`)
+
+ rsp := logoutUser(logoutCmd, req, user).(*resp)
+ c.Check(rsp.Status, check.Equals, 200)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+
+ state.Lock()
+ _, err = auth.User(state, user.ID)
+ state.Unlock()
+ c.Check(err, check.ErrorMatches, "invalid user")
+}
+
+func (s *apiSuite) TestLoginUserBadRequest(c *check.C) {
+ buf := bytes.NewBufferString(`hello`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Result, check.NotNil)
+}
+
+func (s *apiSuite) TestLoginUserMyAppsError(c *check.C) {
+ mockMyAppsServer := s.makeMyAppsServer(200, "{}")
+ defer mockMyAppsServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusUnauthorized)
+ c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot get snap access permission")
+}
+
+func (s *apiSuite) TestLoginUserTwoFactorRequiredError(c *check.C) {
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"code": "TWOFACTOR_REQUIRED"}`
+ mockSSOServer := s.makeSSOServer(401, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusUnauthorized)
+ c.Check(rsp.Result.(*errorResult).Kind, check.Equals, errorKindTwoFactorRequired)
+}
+
+func (s *apiSuite) TestLoginUserTwoFactorFailedError(c *check.C) {
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"code": "TWOFACTOR_FAILURE"}`
+ mockSSOServer := s.makeSSOServer(403, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusUnauthorized)
+ c.Check(rsp.Result.(*errorResult).Kind, check.Equals, errorKindTwoFactorFailed)
+}
+
+func (s *apiSuite) TestLoginUserInvalidCredentialsError(c *check.C) {
+ serializedMacaroon, err := s.makeStoreMacaroon()
+ c.Assert(err, check.IsNil)
+ responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon)
+ c.Assert(err, check.IsNil)
+ mockMyAppsServer := s.makeMyAppsServer(200, responseData)
+ defer mockMyAppsServer.Close()
+
+ discharge := `{"code": "INVALID_CREDENTIALS"}`
+ mockSSOServer := s.makeSSOServer(401, discharge)
+ defer mockSSOServer.Close()
+
+ buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := loginUser(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusUnauthorized)
+ c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot authenticate to snap store")
+}
+
+func (s *apiSuite) TestUserFromRequestNoHeader(c *check.C) {
+ req, _ := http.NewRequest("GET", "http://example.com", nil)
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := UserFromRequest(state, req)
+ state.Unlock()
+
+ c.Check(err, check.Equals, auth.ErrInvalidAuth)
+ c.Check(user, check.IsNil)
+}
+
+func (s *apiSuite) TestUserFromRequestHeaderNoMacaroons(c *check.C) {
+ req, _ := http.NewRequest("GET", "http://example.com", nil)
+ req.Header.Set("Authorization", "Invalid")
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := UserFromRequest(state, req)
+ state.Unlock()
+
+ c.Check(err, check.ErrorMatches, "authorization header misses Macaroon prefix")
+ c.Check(user, check.IsNil)
+}
+
+func (s *apiSuite) TestUserFromRequestHeaderIncomplete(c *check.C) {
+ req, _ := http.NewRequest("GET", "http://example.com", nil)
+ req.Header.Set("Authorization", `Macaroon root=""`)
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := UserFromRequest(state, req)
+ state.Unlock()
+
+ c.Check(err, check.ErrorMatches, "invalid authorization header")
+ c.Check(user, check.IsNil)
+}
+
+func (s *apiSuite) TestUserFromRequestHeaderCorrectMissingUser(c *check.C) {
+ req, _ := http.NewRequest("GET", "http://example.com", nil)
+ req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`)
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := UserFromRequest(state, req)
+ state.Unlock()
+
+ c.Check(err, check.Equals, auth.ErrInvalidAuth)
+ c.Check(user, check.IsNil)
+}
+
+func (s *apiSuite) TestUserFromRequestHeaderValidUser(c *check.C) {
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ expectedUser, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ req, _ := http.NewRequest("GET", "http://example.com", nil)
+ req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, expectedUser.Macaroon))
+
+ state.Lock()
+ user, err := UserFromRequest(state, req)
+ state.Unlock()
+
+ c.Check(err, check.IsNil)
+ c.Check(user, check.DeepEquals, expectedUser)
+}
+
+func (s *apiSuite) TestSnapsInfoOnePerIntegration(c *check.C) {
+ d := s.daemon(c)
+
+ req, err := http.NewRequest("GET", "/v2/snaps", nil)
+ c.Assert(err, check.IsNil)
+
+ type tsnap struct {
+ name string
+ dev string
+ ver string
+ rev int
+ }
+
+ tsnaps := []tsnap{
+ {"foo", "bar", "v1", 5},
+ {"bar", "baz", "v2", 10},
+ {"baz", "qux", "v3", 15},
+ {"qux", "mip", "v4", 20},
+ }
+
+ for _, snp := range tsnaps {
+ s.mkInstalledInState(c, d, snp.name, snp.dev, snp.ver, snap.R(snp.rev), false, "")
+ }
+
+ rsp, ok := getSnapsInfo(snapsCmd, req, nil).(*resp)
+ c.Assert(ok, check.Equals, true)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Check(rsp.Result, check.NotNil)
+
+ snaps := snapList(rsp.Result)
+ c.Check(snaps, check.HasLen, len(tsnaps))
+
+ for _, s := range tsnaps {
+ var got map[string]interface{}
+ for _, got = range snaps {
+ if got["name"].(string) == s.name {
+ break
+ }
+ }
+ c.Check(got["name"], check.Equals, s.name)
+ c.Check(got["version"], check.Equals, s.ver)
+ c.Check(got["revision"], check.Equals, snap.R(s.rev).String())
+ c.Check(got["developer"], check.Equals, s.dev)
+ c.Check(got["confinement"], check.Equals, "strict")
+ }
+}
+
+func (s *apiSuite) TestSnapsInfoOnlyLocal(c *check.C) {
+ d := s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ }}
+ s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
+
+ req, err := http.NewRequest("GET", "/v2/snaps?sources=local", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+
+ c.Assert(rsp.Sources, check.DeepEquals, []string{"local"})
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Assert(snaps[0]["name"], check.Equals, "local")
+}
+
+func (s *apiSuite) TestSnapsInfoAll(c *check.C) {
+ d := s.daemon(c)
+
+ s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(1), false, "")
+ s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(2), false, "")
+ s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(3), true, "")
+
+ for _, t := range []struct {
+ q string
+ numSnaps int
+ typ ResponseType
+ }{
+ {"?select=enabled", 1, "sync"},
+ {`?select=`, 1, "sync"},
+ {"", 1, "sync"},
+ {"?select=all", 3, "sync"},
+ {"?select=invalid-field", 0, "error"},
+ } {
+ req, err := http.NewRequest("GET", fmt.Sprintf("/v2/snaps%s", t.q), nil)
+ c.Assert(err, check.IsNil)
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, t.typ)
+
+ if rsp.Type != "error" {
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, t.numSnaps)
+ c.Assert(snaps[0]["name"], check.Equals, "local")
+ }
+ }
+}
+
+func (s *apiSuite) TestFind(c *check.C) {
+ s.suggestedCurrency = "EUR"
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ }}
+
+ req, err := http.NewRequest("GET", "/v2/find?q=hi", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Assert(snaps[0]["name"], check.Equals, "store")
+ c.Check(snaps[0]["prices"], check.IsNil)
+ c.Check(snaps[0]["screenshots"], check.IsNil)
+ c.Check(snaps[0]["channels"], check.IsNil)
+
+ c.Check(rsp.SuggestedCurrency, check.Equals, "EUR")
+
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"})
+ c.Check(s.refreshCandidates, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestFindRefreshes(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ }}
+ s.mockSnap(c, "name: store\nversion: 1.0")
+
+ req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Assert(snaps[0]["name"], check.Equals, "store")
+ c.Check(s.refreshCandidates, check.HasLen, 1)
+}
+
+func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ }}
+
+ s.mockSnap(c, "name: store\nversion: 1.0")
+
+ var snapst snapstate.SnapState
+ st := s.d.overlord.State()
+ st.Lock()
+ err := snapstate.Get(st, "store", &snapst)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Assert(snapst.Sequence, check.HasLen, 1)
+
+ // clear the snapid
+ snapst.Sequence[0].SnapID = ""
+ st.Lock()
+ snapstate.Set(st, "store", &snapst)
+ st.Unlock()
+
+ req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Assert(snaps[0]["name"], check.Equals, "store")
+ c.Check(s.refreshCandidates, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestFindPrivate(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{}
+
+ req, err := http.NewRequest("GET", "/v2/find?q=foo&select=private", nil)
+ c.Assert(err, check.IsNil)
+
+ _ = searchStore(findCmd, req, nil).(*resp)
+
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{
+ Query: "foo",
+ Private: true,
+ })
+}
+
+func (s *apiSuite) TestFindPrefix(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{}
+
+ req, err := http.NewRequest("GET", "/v2/find?name=foo*", nil)
+ c.Assert(err, check.IsNil)
+
+ _ = searchStore(findCmd, req, nil).(*resp)
+
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo", Prefix: true})
+}
+
+func (s *apiSuite) TestFindSection(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{}
+
+ req, err := http.NewRequest("GET", "/v2/find?q=foo§ion=bar", nil)
+ c.Assert(err, check.IsNil)
+
+ _ = searchStore(findCmd, req, nil).(*resp)
+
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{
+ Query: "foo",
+ Section: "bar",
+ })
+}
+
+func (s *apiSuite) TestFindOne(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ Channels: map[string]*snap.ChannelSnapInfo{
+ "stable": {
+ Revision: snap.R(42),
+ },
+ },
+ }}
+ s.mockSnap(c, "name: store\nversion: 1.0")
+
+ req, err := http.NewRequest("GET", "/v2/find?name=foo", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{})
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Check(snaps[0]["name"], check.Equals, "store")
+ m := snaps[0]["channels"].(map[string]interface{})["stable"].(map[string]interface{})
+
+ c.Check(m["revision"], check.Equals, "42")
+}
+
+func (s *apiSuite) TestFindRefreshNotQ(c *check.C) {
+ req, err := http.NewRequest("GET", "/v2/find?select=refresh&q=foo", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, "cannot use 'q' with 'select=refresh'")
+}
+
+func (s *apiSuite) TestFindPriced(c *check.C) {
+ s.suggestedCurrency = "GBP"
+
+ s.rsnaps = []*snap.Info{{
+ Type: snap.TypeApp,
+ Version: "v2",
+ Prices: map[string]float64{
+ "GBP": 1.23,
+ "EUR": 2.34,
+ },
+ MustBuy: true,
+ SideInfo: snap.SideInfo{
+ RealName: "banana",
+ },
+ Publisher: "foo",
+ }}
+
+ req, err := http.NewRequest("GET", "/v2/find?q=banana&channel=stable", nil)
+ c.Assert(err, check.IsNil)
+ rsp, ok := searchStore(findCmd, req, nil).(*resp)
+ c.Assert(ok, check.Equals, true)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+
+ snap := snaps[0]
+ c.Check(snap["name"], check.Equals, "banana")
+ c.Check(snap["prices"], check.DeepEquals, map[string]interface{}{
+ "EUR": 2.34,
+ "GBP": 1.23,
+ })
+ c.Check(snap["status"], check.Equals, "priced")
+
+ c.Check(rsp.SuggestedCurrency, check.Equals, "GBP")
+}
+
+func (s *apiSuite) TestFindScreenshotted(c *check.C) {
+ s.rsnaps = []*snap.Info{{
+ Type: snap.TypeApp,
+ Version: "v2",
+ Screenshots: []snap.ScreenshotInfo{
+ {
+ URL: "http://example.com/screenshot.png",
+ Width: 800,
+ Height: 1280,
+ },
+ {
+ URL: "http://example.com/screenshot2.png",
+ },
+ },
+ MustBuy: true,
+ SideInfo: snap.SideInfo{
+ RealName: "test-screenshot",
+ },
+ Publisher: "foo",
+ }}
+
+ req, err := http.NewRequest("GET", "/v2/find?q=test-screenshot", nil)
+ c.Assert(err, check.IsNil)
+ rsp, ok := searchStore(findCmd, req, nil).(*resp)
+ c.Assert(ok, check.Equals, true)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+
+ c.Check(snaps[0]["name"], check.Equals, "test-screenshot")
+ c.Check(snaps[0]["screenshots"], check.DeepEquals, []interface{}{
+ map[string]interface{}{
+ "url": "http://example.com/screenshot.png",
+ "width": float64(800),
+ "height": float64(1280),
+ },
+ map[string]interface{}{
+ "url": "http://example.com/screenshot2.png",
+ },
+ })
+}
+
+func (s *apiSuite) TestSnapsInfoOnlyStore(c *check.C) {
+ d := s.daemon(c)
+
+ s.suggestedCurrency = "EUR"
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ }}
+ s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
+
+ req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+
+ c.Assert(rsp.Sources, check.DeepEquals, []string{"store"})
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Assert(snaps[0]["name"], check.Equals, "store")
+ c.Check(snaps[0]["prices"], check.IsNil)
+
+ c.Check(rsp.SuggestedCurrency, check.Equals, "EUR")
+}
+
+func (s *apiSuite) TestSnapsStoreConfinement(c *check.C) {
+ s.rsnaps = []*snap.Info{
+ {
+ // no explicit confinement in this one
+ SideInfo: snap.SideInfo{
+ RealName: "foo",
+ },
+ },
+ {
+ Confinement: snap.StrictConfinement,
+ SideInfo: snap.SideInfo{
+ RealName: "bar",
+ },
+ },
+ {
+ Confinement: snap.DevModeConfinement,
+ SideInfo: snap.SideInfo{
+ RealName: "baz",
+ },
+ },
+ }
+
+ req, err := http.NewRequest("GET", "/v2/find", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 3)
+
+ for i, ss := range [][2]string{
+ {"foo", string(snap.StrictConfinement)},
+ {"bar", string(snap.StrictConfinement)},
+ {"baz", string(snap.DevModeConfinement)},
+ } {
+ name, mode := ss[0], ss[1]
+ c.Check(snaps[i]["name"], check.Equals, name, check.Commentf(name))
+ c.Check(snaps[i]["confinement"], check.Equals, mode, check.Commentf(name))
+ }
+}
+
+func (s *apiSuite) TestSnapsInfoStoreWithAuth(c *check.C) {
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(s.user, check.IsNil)
+
+ _ = getSnapsInfo(snapsCmd, req, user).(*resp)
+
+ // ensure user was set
+ c.Assert(s.user, check.DeepEquals, user)
+}
+
+func (s *apiSuite) TestSnapsInfoLocalAndStore(c *check.C) {
+ d := s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ Version: "v42",
+ SideInfo: snap.SideInfo{
+ RealName: "remote",
+ },
+ Publisher: "foo",
+ }}
+ s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
+
+ req, err := http.NewRequest("GET", "/v2/snaps?sources=local,store", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+
+ // presence of 'store' in sources bounces request over to /find
+ c.Assert(rsp.Sources, check.DeepEquals, []string{"store"})
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Check(snaps[0]["version"], check.Equals, "v42")
+
+ // as does a 'q'
+ req, err = http.NewRequest("GET", "/v2/snaps?q=what", nil)
+ c.Assert(err, check.IsNil)
+ rsp = getSnapsInfo(snapsCmd, req, nil).(*resp)
+ snaps = snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Check(snaps[0]["version"], check.Equals, "v42")
+
+ // otherwise, local only
+ req, err = http.NewRequest("GET", "/v2/snaps", nil)
+ c.Assert(err, check.IsNil)
+ rsp = getSnapsInfo(snapsCmd, req, nil).(*resp)
+ snaps = snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Check(snaps[0]["version"], check.Equals, "v1")
+}
+
+func (s *apiSuite) TestSnapsInfoDefaultSources(c *check.C) {
+ d := s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "remote",
+ },
+ Publisher: "foo",
+ }}
+ s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
+
+ req, err := http.NewRequest("GET", "/v2/snaps", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+
+ c.Assert(rsp.Sources, check.DeepEquals, []string{"local"})
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+}
+
+func (s *apiSuite) TestSnapsInfoUnknownSource(c *check.C) {
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "remote",
+ },
+ Publisher: "foo",
+ }}
+ s.mkInstalled(c, "local", "foo", "v1", snap.R(10), true, "")
+
+ req, err := http.NewRequest("GET", "/v2/snaps?sources=unknown", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+
+ c.Check(rsp.Sources, check.DeepEquals, []string{"local"})
+
+ snaps := snapList(rsp.Result)
+ c.Check(snaps, check.HasLen, 1)
+}
+
+func (s *apiSuite) TestSnapsInfoFilterRemote(c *check.C) {
+ s.rsnaps = nil
+
+ req, err := http.NewRequest("GET", "/v2/snaps?q=foo&sources=store", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getSnapsInfo(snapsCmd, req, nil).(*resp)
+
+ c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo"})
+
+ c.Assert(rsp.Result, check.NotNil)
+}
+
+func (s *apiSuite) TestPostSnapBadRequest(c *check.C) {
+ buf := bytes.NewBufferString(`hello`)
+ req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Result, check.NotNil)
+}
+
+func (s *apiSuite) TestPostSnapBadAction(c *check.C) {
+ buf := bytes.NewBufferString(`{"action": "potato"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Result, check.NotNil)
+}
+
+func (s *apiSuite) TestPostSnap(c *check.C) {
+ d := s.daemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ soon := 0
+ ensureStateSoon = func(st *state.State) {
+ soon++
+ ensureStateSoonImpl(st)
+ }
+
+ s.vars = map[string]string{"name": "foo"}
+
+ snapInstructionDispTable["install"] = func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) {
+ return "foooo", nil, nil
+ }
+ defer func() {
+ snapInstructionDispTable["install"] = snapInstall
+ }()
+
+ buf := bytes.NewBufferString(`{"action": "install"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+ c.Check(chg.Summary(), check.Equals, "foooo")
+ var names []string
+ err = chg.Get("snap-names", &names)
+ c.Assert(err, check.IsNil)
+ c.Check(names, check.DeepEquals, []string{"foo"})
+ st.Unlock()
+
+ c.Check(soon, check.Equals, 1)
+}
+
+func (s *apiSuite) TestPostSnapSetsUser(c *check.C) {
+ d := s.daemon(c)
+ ensureStateSoon = func(st *state.State) {}
+
+ snapInstructionDispTable["install"] = func(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ return fmt.Sprintf("<install by user %d>", inst.userID), nil, nil
+ }
+ defer func() {
+ snapInstructionDispTable["install"] = snapInstall
+ }()
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ buf := bytes.NewBufferString(`{"action": "install"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`)
+
+ rsp := postSnap(snapCmd, req, user).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+ c.Check(chg.Summary(), check.Equals, "<install by user 1>")
+ st.Unlock()
+}
+
+func (s *apiSuite) TestPostSnapDispatch(c *check.C) {
+ inst := &snapInstruction{Snaps: []string{"foo"}}
+
+ type T struct {
+ s string
+ impl snapActionFunc
+ }
+
+ actions := []T{
+ {"install", snapInstall},
+ {"refresh", snapUpdate},
+ {"remove", snapRemove},
+ {"revert", snapRevert},
+ {"enable", snapEnable},
+ {"disable", snapDisable},
+ {"xyzzy", nil},
+ }
+
+ for _, action := range actions {
+ inst.Action = action.s
+ // do you feel dirty yet?
+ c.Check(fmt.Sprintf("%p", action.impl), check.Equals, fmt.Sprintf("%p", inst.dispatch()))
+ }
+}
+
+func (s *apiSuite) TestPostSnapEnableDisableRevision(c *check.C) {
+ for _, action := range []string{"enable", "disable"} {
+ buf := bytes.NewBufferString(`{"action": "` + action + `", "revision": "42"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "takes no revision")
+ }
+}
+
+var sideLoadBodyWithoutDevMode = "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"dangerous\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap-path\"\r\n" +
+ "\r\n" +
+ "a/b/local.snap\r\n" +
+ "----hello--\r\n"
+
+func (s *apiSuite) TestSideloadSnapOnNonDevModeDistro(c *check.C) {
+ // try a multipart/form-data upload
+ body := sideLoadBodyWithoutDevMode
+ head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
+ chgSummary := s.sideloadCheck(c, body, head, snapstate.Flags{RemoveSnapPath: true}, false)
+ c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`)
+}
+
+func (s *apiSuite) TestSideloadSnapOnDevModeDistro(c *check.C) {
+ // try a multipart/form-data upload
+ body := sideLoadBodyWithoutDevMode
+ head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
+ restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"})
+ defer restore()
+ flags := snapstate.Flags{DevMode: true, RemoveSnapPath: true}
+ chgSummary := s.sideloadCheck(c, body, head, flags, false)
+ c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`)
+}
+
+func (s *apiSuite) TestSideloadSnapDevMode(c *check.C) {
+ body := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"devmode\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n"
+ head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
+ // try a multipart/form-data upload
+ restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"})
+ defer restore()
+ flags := snapstate.Flags{RemoveSnapPath: true}
+ flags.DevMode = true
+ chgSummary := s.sideloadCheck(c, body, head, flags, true)
+ c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`)
+}
+
+func (s *apiSuite) TestSideloadSnapJailMode(c *check.C) {
+ body := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"jailmode\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"dangerous\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n"
+ head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
+ // try a multipart/form-data upload
+ flags := snapstate.Flags{JailMode: true, RemoveSnapPath: true}
+ chgSummary := s.sideloadCheck(c, body, head, flags, true)
+ c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`)
+}
+
+func (s *apiSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) {
+ body := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"jailmode\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"devmode\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n"
+ d := newTestDaemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
+
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot use devmode and jailmode flags together")
+}
+
+func (s *apiSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) {
+ body := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"jailmode\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n"
+ d := newTestDaemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
+
+ restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"})
+ defer restore()
+
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, check.Equals, "this system cannot honour the jailmode flag")
+}
+
+func (s *apiSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) {
+ d := newTestDaemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+ // add the assertions first
+ st := d.overlord.State()
+ assertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ dev1Acct := assertstest.NewAccount(s.storeSigning, "devel1", nil, "")
+ assertAdd(st, dev1Acct)
+
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "x-id",
+ "snap-name": "x",
+ "publisher-id": dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ assertAdd(st, snapDecl)
+
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": "YK0GWATaZf09g_fvspYPqm_qtaiqf-KjaNj5uMEQCjQpuXWPjqQbeBINL5H_A0Lo",
+ "snap-size": "5",
+ "snap-id": "x-id",
+ "snap-revision": "41",
+ "developer-id": dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ assertAdd(st, snapRev)
+
+ body := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x.snap\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n"
+ req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ return nil, nil
+ }
+ snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, channel string, flags snapstate.Flags) (*state.TaskSet, error) {
+ c.Check(flags, check.Equals, snapstate.Flags{RemoveSnapPath: true})
+ c.Check(si, check.DeepEquals, &snap.SideInfo{
+ RealName: "x",
+ SnapID: "x-id",
+ Revision: snap.R(41),
+ })
+
+ return state.NewTaskSet(), nil
+ }
+
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+ c.Check(chg.Summary(), check.Equals, `Install "x" snap from file "x.snap"`)
+ var names []string
+ err = chg.Get("snap-names", &names)
+ c.Assert(err, check.IsNil)
+ c.Check(names, check.DeepEquals, []string{"x"})
+ var apiData map[string]interface{}
+ err = chg.Get("api-data", &apiData)
+ c.Assert(err, check.IsNil)
+ c.Check(apiData, check.DeepEquals, map[string]interface{}{
+ "snap-name": "x",
+ })
+}
+
+func (s *apiSuite) TestSideloadSnapNoSignaturesDangerOff(c *check.C) {
+ body := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n"
+ d := newTestDaemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
+
+ // this is the prefix used for tempfiles for sideloading
+ glob := filepath.Join(os.TempDir(), "snapd-sideload-pkg-*")
+ glbBefore, _ := filepath.Glob(glob)
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, check.Equals, `cannot find signatures with metadata for snap "x"`)
+ glbAfter, _ := filepath.Glob(glob)
+ c.Check(len(glbBefore), check.Equals, len(glbAfter))
+}
+
+func (s *apiSuite) TestSideloadSnapNotValidFormFile(c *check.C) {
+ newTestDaemon(c)
+
+ // try a multipart/form-data upload with missing "name"
+ content := "" +
+ "----hello--\r\n" +
+ "Content-Disposition: form-data; filename=\"x\"\r\n" +
+ "\r\n" +
+ "xyzzy\r\n" +
+ "----hello--\r\n"
+ head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
+
+ buf := bytes.NewBufferString(content)
+ req, err := http.NewRequest("POST", "/v2/snaps", buf)
+ c.Assert(err, check.IsNil)
+ for k, v := range head {
+ req.Header.Set(k, v)
+ }
+
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeError)
+ c.Assert(rsp.Result.(*errorResult).Message, check.Matches, `cannot find "snap" file field in provided multipart/form-data payload`)
+}
+
+func (s *apiSuite) TestTrySnap(c *check.C) {
+ d := newTestDaemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ req, err := http.NewRequest("POST", "/v2/snaps", nil)
+ c.Assert(err, check.IsNil)
+
+ // mock a try dir
+ tryDir := c.MkDir()
+ snapYaml := filepath.Join(tryDir, "meta", "snap.yaml")
+ err = os.MkdirAll(filepath.Dir(snapYaml), 0755)
+ c.Assert(err, check.IsNil)
+ err = ioutil.WriteFile(snapYaml, []byte("name: foo\nversion: 1.0\n"), 0644)
+ c.Assert(err, check.IsNil)
+
+ for _, t := range []struct {
+ coreInfoErr error
+ nTasks int
+ installSnap string
+ }{
+ // core installed
+ {nil, 1, ""},
+ // no-core-installed
+ {state.ErrNoState, 2, "core"},
+ } {
+ soon := 0
+ ensureStateSoon = func(st *state.State) {
+ soon++
+ ensureStateSoonImpl(st)
+ }
+
+ tryWasCalled := true
+ snapstateTryPath = func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) {
+ tryWasCalled = true
+ t := s.NewTask("fake-install-snap", "Doing a fake try")
+ return state.NewTaskSet(t), nil
+ }
+
+ installSnap := ""
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ installSnap = name
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ return nil, t.coreInfoErr
+ }
+
+ // try the snap (without an installed core)
+ rsp := trySnap(snapsCmd, req, nil, tryDir, snapstate.Flags{}).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeAsync)
+ c.Assert(tryWasCalled, check.Equals, true)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Assert(chg.Tasks(), check.HasLen, t.nTasks)
+ c.Check(installSnap, check.Equals, t.installSnap)
+
+ st.Unlock()
+ <-chg.Ready()
+ st.Lock()
+
+ c.Check(chg.Kind(), check.Equals, "try-snap")
+ c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Try "%s" snap from %s`, "foo", tryDir))
+ var names []string
+ err = chg.Get("snap-names", &names)
+ c.Assert(err, check.IsNil)
+ c.Check(names, check.DeepEquals, []string{"foo"})
+ var apiData map[string]interface{}
+ err = chg.Get("api-data", &apiData)
+ c.Assert(err, check.IsNil)
+ c.Check(apiData, check.DeepEquals, map[string]interface{}{
+ "snap-name": "foo",
+ })
+
+ c.Check(soon, check.Equals, 1)
+ st.Unlock()
+ }
+}
+
+func (s *apiSuite) TestTrySnapRelative(c *check.C) {
+ req, err := http.NewRequest("POST", "/v2/snaps", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := trySnap(snapsCmd, req, nil, "relative-path", snapstate.Flags{}).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "need an absolute path")
+}
+
+func (s *apiSuite) TestTrySnapNotDir(c *check.C) {
+ req, err := http.NewRequest("POST", "/v2/snaps", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := trySnap(snapsCmd, req, nil, "/does/not/exist", snapstate.Flags{}).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "not a snap directory")
+}
+
+func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snapstate.Flags, hasCoreSnap bool) string {
+ d := newTestDaemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ soon := 0
+ ensureStateSoon = func(st *state.State) {
+ soon++
+ ensureStateSoonImpl(st)
+ }
+
+ // setup done
+ installQueue := []string{}
+ unsafeReadSnapInfo = func(path string) (*snap.Info, error) {
+ return &snap.Info{SuggestedName: "local"}, nil
+ }
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ if hasCoreSnap {
+ return nil, nil
+ }
+ // pretend we do not have a state for ubuntu-core
+ return nil, state.ErrNoState
+ }
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ // NOTE: ubuntu-core is not installed in developer mode
+ c.Check(flags, check.Equals, snapstate.Flags{})
+ installQueue = append(installQueue, name)
+
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, channel string, flags snapstate.Flags) (*state.TaskSet, error) {
+ c.Check(flags, check.DeepEquals, expectedFlags)
+
+ bs, err := ioutil.ReadFile(path)
+ c.Check(err, check.IsNil)
+ c.Check(string(bs), check.Equals, "xyzzy")
+
+ installQueue = append(installQueue, si.RealName+"::"+path)
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ buf := bytes.NewBufferString(content)
+ req, err := http.NewRequest("POST", "/v2/snaps", buf)
+ c.Assert(err, check.IsNil)
+ for k, v := range head {
+ req.Header.Set(k, v)
+ }
+
+ rsp := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(rsp.Type, check.Equals, ResponseTypeAsync)
+ n := 1
+ if !hasCoreSnap {
+ n++
+ }
+ c.Assert(installQueue, check.HasLen, n)
+ if !hasCoreSnap {
+ c.Check(installQueue[0], check.Equals, defaultCoreSnapName)
+ }
+ c.Check(installQueue[n-1], check.Matches, "local::.*/snapd-sideload-pkg-.*")
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Check(soon, check.Equals, 1)
+
+ c.Assert(chg.Tasks(), check.HasLen, n)
+
+ st.Unlock()
+ <-chg.Ready()
+ st.Lock()
+
+ c.Check(chg.Kind(), check.Equals, "install-snap")
+ var names []string
+ err = chg.Get("snap-names", &names)
+ c.Assert(err, check.IsNil)
+ c.Check(names, check.DeepEquals, []string{"local"})
+ var apiData map[string]interface{}
+ err = chg.Get("api-data", &apiData)
+ c.Assert(err, check.IsNil)
+ c.Check(apiData, check.DeepEquals, map[string]interface{}{
+ "snap-name": "local",
+ })
+
+ return chg.Summary()
+}
+
+func (s *apiSuite) runGetConf(c *check.C, keys []string) map[string]interface{} {
+ s.vars = map[string]string{"name": "test-snap"}
+ req, err := http.NewRequest("GET", "/v2/snaps/test-snap/conf?keys="+strings.Join(keys, ","), nil)
+ c.Check(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ snapConfCmd.GET(snapConfCmd, req, nil).ServeHTTP(rec, req)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ return body["result"].(map[string]interface{})
+}
+
+func (s *apiSuite) TestGetConfSingleKey(c *check.C) {
+ d := s.daemon(c)
+
+ // Set a config that we'll get in a moment
+ d.overlord.State().Lock()
+ transaction := configstate.NewTransaction(d.overlord.State())
+ transaction.Set("test-snap", "test-key1", "test-value1")
+ transaction.Set("test-snap", "test-key2", "test-value2")
+ transaction.Commit()
+ d.overlord.State().Unlock()
+
+ result := s.runGetConf(c, []string{"test-key1"})
+ c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"})
+
+ result = s.runGetConf(c, []string{"test-key1", "test-key2"})
+ c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"})
+}
+
+func (s *apiSuite) TestSetConf(c *check.C) {
+ d := s.daemon(c)
+ s.mockSnap(c, configYaml)
+
+ // Mock the hook runner
+ hookRunner := testutil.MockCommand(c, "snap", "")
+ defer hookRunner.Restore()
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ text, err := json.Marshal(map[string]interface{}{"key": "value"})
+ c.Assert(err, check.IsNil)
+
+ buffer := bytes.NewBuffer(text)
+ req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer)
+ c.Assert(err, check.IsNil)
+
+ s.vars = map[string]string{"name": "config-snap"}
+
+ rec := httptest.NewRecorder()
+ snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+
+ // Check that the configure hook was run correctly
+ c.Check(hookRunner.Calls(), check.DeepEquals, [][]string{{
+ "snap", "run", "--hook", "configure", "-r", "unset", "config-snap",
+ }})
+}
+
+func (s *apiSuite) TestAppIconGet(c *check.C) {
+ d := s.daemon(c)
+
+ // have an active foo in the system
+ info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "")
+
+ // have an icon for it in the package itself
+ iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick")
+ c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil)
+ c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil)
+
+ s.vars = map[string]string{"name": "foo"}
+ req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil)
+ c.Assert(err, check.IsNil)
+
+ rec := httptest.NewRecorder()
+
+ appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 200)
+ c.Check(rec.Body.String(), check.Equals, "ick")
+}
+
+func (s *apiSuite) TestAppIconGetInactive(c *check.C) {
+ d := s.daemon(c)
+
+ // have an *in*active foo in the system
+ info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), false, "")
+
+ // have an icon for it in the package itself
+ iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick")
+ c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil)
+ c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil)
+
+ s.vars = map[string]string{"name": "foo"}
+ req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil)
+ c.Assert(err, check.IsNil)
+
+ rec := httptest.NewRecorder()
+
+ appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 200)
+ c.Check(rec.Body.String(), check.Equals, "ick")
+}
+
+func (s *apiSuite) TestAppIconGetNoIcon(c *check.C) {
+ d := s.daemon(c)
+
+ // have an *in*active foo in the system
+ info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "")
+
+ // NO ICON!
+ err := os.RemoveAll(filepath.Join(info.MountDir(), "meta", "gui", "icon.svg"))
+ c.Assert(err, check.IsNil)
+
+ s.vars = map[string]string{"name": "foo"}
+ req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil)
+ c.Assert(err, check.IsNil)
+
+ rec := httptest.NewRecorder()
+
+ appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code/100, check.Equals, 4)
+}
+
+func (s *apiSuite) TestAppIconGetNoApp(c *check.C) {
+ s.daemon(c)
+
+ s.vars = map[string]string{"name": "foo"}
+ req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil)
+ c.Assert(err, check.IsNil)
+
+ rec := httptest.NewRecorder()
+
+ appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 404)
+}
+
+func (s *apiSuite) TestNotInstalledSnapIcon(c *check.C) {
+ info := &snap.Info{SuggestedName: "notInstalledSnap", IconURL: "icon.svg"}
+ iconfile := snapIcon(info)
+ c.Check(iconfile, testutil.Contains, "icon.svg")
+}
+
+func (s *apiSuite) TestInstallOnNonDevModeDistro(c *check.C) {
+ s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags{}, snap.R(0))
+}
+func (s *apiSuite) TestInstallOnDevModeDistro(c *check.C) {
+ flags := snapstate.Flags{}
+ flags.DevMode = true
+ s.testInstall(c, &release.OS{ID: "x-devmode-distro"}, flags, snap.R(0))
+}
+func (s *apiSuite) TestInstallRevision(c *check.C) {
+ s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags{}, snap.R(42))
+}
+
+func (s *apiSuite) testInstall(c *check.C, releaseInfo *release.OS, flags snapstate.Flags, revision snap.Revision) {
+ calledFlags := snapstate.Flags{}
+ installQueue := []string{}
+ restore := release.MockReleaseInfo(releaseInfo)
+ defer restore()
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // we have core
+ return nil, nil
+ }
+ snapstateInstall = func(s *state.State, name, channel string, revno snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+ installQueue = append(installQueue, name)
+ c.Check(revision, check.Equals, revno)
+
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ d := s.daemon(c)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ var buf bytes.Buffer
+ if revision.Unset() {
+ buf.WriteString(`{"action": "install"}`)
+ } else {
+ fmt.Fprintf(&buf, `{"action": "install", "revision": %s}`, revision.String())
+ }
+ req, err := http.NewRequest("POST", "/v2/snaps/some-snap", &buf)
+ c.Assert(err, check.IsNil)
+
+ s.vars = map[string]string{"name": "some-snap"}
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Assert(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Check(chg.Tasks(), check.HasLen, 1)
+
+ st.Unlock()
+ <-chg.Ready()
+ st.Lock()
+
+ c.Check(chg.Status(), check.Equals, state.DoneStatus)
+ c.Check(calledFlags, check.Equals, flags)
+ c.Check(err, check.IsNil)
+ c.Check(installQueue, check.DeepEquals, []string{"some-snap"})
+ c.Check(chg.Kind(), check.Equals, "install-snap")
+ c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap`)
+}
+
+func (s *apiSuite) TestRefresh(c *check.C) {
+ var calledFlags snapstate.Flags
+ calledUserID := 0
+ installQueue := []string{}
+ assertstateCalledUserID := 0
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // we have core
+ return nil, nil
+ }
+ snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+ calledUserID = userID
+ installQueue = append(installQueue, name)
+
+ t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ assertstateCalledUserID = userID
+ return nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "refresh",
+ Snaps: []string{"some-snap"},
+ userID: 17,
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ summary, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ c.Check(assertstateCalledUserID, check.Equals, 17)
+ c.Check(calledFlags, check.DeepEquals, snapstate.Flags{})
+ c.Check(calledUserID, check.Equals, 17)
+ c.Check(err, check.IsNil)
+ c.Check(installQueue, check.DeepEquals, []string{"some-snap"})
+ c.Check(summary, check.Equals, `Refresh "some-snap" snap`)
+}
+
+func (s *apiSuite) TestRefreshDevMode(c *check.C) {
+ var calledFlags snapstate.Flags
+ calledUserID := 0
+ installQueue := []string{}
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // we have core
+ return nil, nil
+ }
+ snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+ calledUserID = userID
+ installQueue = append(installQueue, name)
+
+ t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ return nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "refresh",
+ DevMode: true,
+ Snaps: []string{"some-snap"},
+ userID: 17,
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ summary, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ flags := snapstate.Flags{}
+ flags.DevMode = true
+ c.Check(calledFlags, check.DeepEquals, flags)
+ c.Check(calledUserID, check.Equals, 17)
+ c.Check(err, check.IsNil)
+ c.Check(installQueue, check.DeepEquals, []string{"some-snap"})
+ c.Check(summary, check.Equals, `Refresh "some-snap" snap`)
+}
+
+func (s *apiSuite) TestRefreshClassic(c *check.C) {
+ var calledFlags snapstate.Flags
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // we have ubuntu-core
+ return nil, nil
+ }
+ snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+ return nil, nil
+ }
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ return nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "refresh",
+ Classic: true,
+ Snaps: []string{"some-snap"},
+ userID: 17,
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ c.Check(calledFlags, check.DeepEquals, snapstate.Flags{Classic: true})
+}
+
+func (s *apiSuite) TestRefreshIgnoreValidation(c *check.C) {
+ var calledFlags snapstate.Flags
+ calledUserID := 0
+ installQueue := []string{}
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // we have ubuntu-core
+ return nil, nil
+ }
+ snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+ calledUserID = userID
+ installQueue = append(installQueue, name)
+
+ t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ return nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "refresh",
+ IgnoreValidation: true,
+ Snaps: []string{"some-snap"},
+ userID: 17,
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ summary, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ flags := snapstate.Flags{}
+ flags.IgnoreValidation = true
+
+ c.Check(calledFlags, check.DeepEquals, flags)
+ c.Check(calledUserID, check.Equals, 17)
+ c.Check(err, check.IsNil)
+ c.Check(installQueue, check.DeepEquals, []string{"some-snap"})
+ c.Check(summary, check.Equals, `Refresh "some-snap" snap`)
+}
+
+func (s *apiSuite) TestPostSnapsOp(c *check.C) {
+ snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 0)
+ t := s.NewTask("fake-refresh-all", "Refreshing everything")
+ return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ }
+
+ d := s.daemon(c)
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ buf := bytes.NewBufferString(`{"action": "refresh"}`)
+ req, err := http.NewRequest("POST", "/v2/login", buf)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "application/json")
+
+ rsp, ok := postSnaps(snapsCmd, req, nil).(*resp)
+ c.Assert(ok, check.Equals, true)
+ c.Check(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Check(chg.Summary(), check.Equals, `Refresh snaps "fake1", "fake2"`)
+ var apiData map[string]interface{}
+ c.Check(chg.Get("api-data", &apiData), check.IsNil)
+ c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"})
+}
+
+func (s *apiSuite) TestRefreshAll(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return assertstate.RefreshSnapDeclarations(s, userID)
+ }
+ d := s.daemon(c)
+
+ for _, tst := range []struct {
+ snaps []string
+ msg string
+ }{
+ {nil, "Refresh all snaps: no updates"},
+ {[]string{"fake"}, `Refresh snap "fake"`},
+ {[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`},
+ } {
+ refreshSnapDecls = false
+
+ snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 0)
+ t := s.NewTask("fake-refresh-all", "Refreshing everything")
+ return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ }
+
+ inst := &snapInstruction{Action: "refresh"}
+ st := d.overlord.State()
+ st.Lock()
+ summary, _, _, err := snapUpdateMany(inst, st)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Check(summary, check.Equals, tst.msg)
+ c.Check(refreshSnapDecls, check.Equals, true)
+ }
+}
+
+func (s *apiSuite) TestRefreshAllNoChanges(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return assertstate.RefreshSnapDeclarations(s, userID)
+ }
+
+ snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 0)
+ return nil, nil, nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{Action: "refresh"}
+ st := d.overlord.State()
+ st.Lock()
+ summary, _, _, err := snapUpdateMany(inst, st)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Check(summary, check.Equals, `Refresh all snaps: no updates`)
+ c.Check(refreshSnapDecls, check.Equals, true)
+}
+
+func (s *apiSuite) TestRefreshMany(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return nil
+ }
+
+ snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 2)
+ t := s.NewTask("fake-refresh-2", "Refreshing two")
+ return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo", "bar"}}
+ st := d.overlord.State()
+ st.Lock()
+ summary, updates, _, err := snapUpdateMany(inst, st)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Check(summary, check.Equals, `Refresh snaps "foo", "bar"`)
+ c.Check(updates, check.DeepEquals, inst.Snaps)
+ c.Check(refreshSnapDecls, check.Equals, true)
+}
+
+func (s *apiSuite) TestRefreshMany1(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return nil
+ }
+
+ snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 1)
+ t := s.NewTask("fake-refresh-1", "Refreshing one")
+ return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo"}}
+ st := d.overlord.State()
+ st.Lock()
+ summary, updates, _, err := snapUpdateMany(inst, st)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Check(summary, check.Equals, `Refresh snap "foo"`)
+ c.Check(updates, check.DeepEquals, inst.Snaps)
+ c.Check(refreshSnapDecls, check.Equals, true)
+}
+
+func (s *apiSuite) TestInstallMany(c *check.C) {
+ snapstateInstallMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 2)
+ t := s.NewTask("fake-install-2", "Install two")
+ return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{Action: "install", Snaps: []string{"foo", "bar"}}
+ st := d.overlord.State()
+ st.Lock()
+ summary, installs, _, err := snapInstallMany(inst, st)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Check(summary, check.Equals, `Install snaps "foo", "bar"`)
+ c.Check(installs, check.DeepEquals, inst.Snaps)
+}
+
+func (s *apiSuite) TestRemoveMany(c *check.C) {
+ snapstateRemoveMany = func(s *state.State, names []string) ([]string, []*state.TaskSet, error) {
+ c.Check(names, check.HasLen, 2)
+ t := s.NewTask("fake-remove-2", "Remove two")
+ return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{Action: "remove", Snaps: []string{"foo", "bar"}}
+ st := d.overlord.State()
+ st.Lock()
+ summary, removes, _, err := snapRemoveMany(inst, st)
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+ c.Check(summary, check.Equals, `Remove snaps "foo", "bar"`)
+ c.Check(removes, check.DeepEquals, inst.Snaps)
+}
+
+func (s *apiSuite) TestInstallMissingCoreSnap(c *check.C) {
+ installQueue := []*state.Task{}
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // pretend we do not have a core
+ return nil, state.ErrNoState
+ }
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ t1 := s.NewTask("fake-install-snap", name)
+ t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks")
+ installQueue = append(installQueue, t1, t2)
+ return state.NewTaskSet(t1, t2), nil
+ }
+
+ d := s.daemon(c)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ buf := bytes.NewBufferString(`{"action": "install"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/some-snap", buf)
+ c.Assert(err, check.IsNil)
+
+ s.vars = map[string]string{"name": "some-snap"}
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Assert(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Check(chg.Tasks(), check.HasLen, 4)
+
+ c.Check(installQueue, check.HasLen, 4)
+ // the two OS snap install tasks
+ c.Check(installQueue[0].Summary(), check.Equals, defaultCoreSnapName)
+ c.Check(installQueue[0].WaitTasks(), check.HasLen, 0)
+ c.Check(installQueue[1].WaitTasks(), check.HasLen, 0)
+ // the two "some-snap" install tasks
+ c.Check(installQueue[2].Summary(), check.Equals, "some-snap")
+ c.Check(installQueue[2].WaitTasks(), check.HasLen, 2)
+ c.Check(installQueue[3].WaitTasks(), check.HasLen, 2)
+}
+
+// Installing ubuntu-core when not having ubuntu-core doesn't misbehave and try
+// to install ubuntu-core twice.
+func (s *apiSuite) TestInstallCoreSnapWhenMissing(c *check.C) {
+ installQueue := []*state.Task{}
+
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // pretend we do not have a core
+ return nil, state.ErrNoState
+ }
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ t1 := s.NewTask("fake-install-snap", name)
+ t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks")
+ installQueue = append(installQueue, t1, t2)
+ return state.NewTaskSet(t1, t2), nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ Snaps: []string{defaultCoreSnapName},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ c.Check(installQueue, check.HasLen, 2)
+ // the only OS snap install tasks
+ c.Check(installQueue[0].Summary(), check.Equals, defaultCoreSnapName)
+ c.Check(installQueue[0].WaitTasks(), check.HasLen, 0)
+ c.Check(installQueue[1].WaitTasks(), check.HasLen, 0)
+}
+
+func (s *apiSuite) TestInstallFails(c *check.C) {
+ snapstateCoreInfo = func(s *state.State) (*snap.Info, error) {
+ // we have core
+ return nil, nil
+ }
+
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ t := s.NewTask("fake-install-snap-error", "Install task")
+ return state.NewTaskSet(t), nil
+ }
+
+ d := s.daemon(c)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ buf := bytes.NewBufferString(`{"action": "install"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postSnap(snapCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeAsync)
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Check(chg.Tasks(), check.HasLen, 1)
+
+ st.Unlock()
+ <-chg.Ready()
+ st.Lock()
+
+ c.Check(chg.Err(), check.ErrorMatches, `(?sm).*Install task \(fake-install-snap-error errored\)`)
+}
+
+func (s *apiSuite) TestInstallLeaveOld(c *check.C) {
+ c.Skip("temporarily dropped half-baked support while sorting out flag mess")
+ var calledFlags snapstate.Flags
+
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ LeaveOld: true,
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Assert(err, check.IsNil)
+
+ c.Check(calledFlags, check.DeepEquals, snapstate.Flags{})
+ c.Check(err, check.IsNil)
+}
+
+func (s *apiSuite) TestInstallDevMode(c *check.C) {
+ var calledFlags snapstate.Flags
+
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ // Install the snap in developer mode
+ DevMode: true,
+ Snaps: []string{"fake"},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ c.Check(calledFlags.DevMode, check.Equals, true)
+}
+
+func (s *apiSuite) TestInstallJailMode(c *check.C) {
+ var calledFlags snapstate.Flags
+
+ snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ JailMode: true,
+ Snaps: []string{"fake"},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ c.Check(calledFlags.JailMode, check.Equals, true)
+}
+
+func (s *apiSuite) TestInstallJailModeDevModeOS(c *check.C) {
+ restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"})
+ defer restore()
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ JailMode: true,
+ Snaps: []string{"foo"},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.ErrorMatches, "this system cannot honour the jailmode flag")
+}
+
+func (s *apiSuite) TestInstallJailModeDevMode(c *check.C) {
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ DevMode: true,
+ JailMode: true,
+ Snaps: []string{"foo"},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.ErrorMatches, "cannot use devmode and jailmode flags together")
+}
+
+func snapList(rawSnaps interface{}) []map[string]interface{} {
+ snaps := make([]map[string]interface{}, len(rawSnaps.([]*json.RawMessage)))
+ for i, raw := range rawSnaps.([]*json.RawMessage) {
+ err := json.Unmarshal([]byte(*raw), &snaps[i])
+ if err != nil {
+ panic(err)
+ }
+ }
+ return snaps
+}
+
+// Tests for GET /v2/interfaces
+
+func (s *apiSuite) TestInterfaces(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ connRef := interfaces.ConnRef{
+ PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
+ }
+ c.Assert(repo.Connect(connRef), check.IsNil)
+
+ req, err := http.NewRequest("GET", "/v2/interfaces", nil)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.GET(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 200)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "producer", "slot": "slot"},
+ },
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ "interface": "test",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "consumer", "plug": "plug"},
+ },
+ },
+ },
+ },
+ "status": "OK",
+ "status-code": 200.0,
+ "type": "sync",
+ })
+}
+
+// Test for POST /v2/interfaces
+
+func (s *apiSuite) TestConnectPlugSuccess(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "connect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, check.HasLen, 1)
+ c.Assert(slot.Connections, check.HasLen, 1)
+ c.Check(plug.Connections[0], check.DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"})
+ c.Check(slot.Connections[0], check.DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"})
+}
+
+func (s *apiSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "different"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, differentProducerYaml)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "connect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+ repo := d.overlord.InterfaceManager().Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, check.HasLen, 0)
+ c.Assert(slot.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ // there is no consumer, no plug defined
+ s.mockSnap(c, producerYaml)
+ s.mockSnap(c, consumerYaml)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "connect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "missingplug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "snap \"consumer\" has no plug named \"missingplug\"",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+
+ repo := d.overlord.InterfaceManager().Repository()
+ slot := repo.Slot("producer", "slot")
+ c.Assert(slot.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ // there is no producer, no slot defined
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "connect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "missingslot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "snap \"producer\" has no slot named \"missingslot\"",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+
+ repo := d.overlord.InterfaceManager().Repository()
+ plug := repo.Plug("consumer", "plug")
+ c.Assert(plug.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestDisconnectPlugSuccess(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ connRef := interfaces.ConnRef{
+ PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
+ }
+ c.Assert(repo.Connect(connRef), check.IsNil)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "disconnect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, check.HasLen, 0)
+ c.Assert(slot.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ // there is no consumer, no plug defined
+ s.mockSnap(c, producerYaml)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "disconnect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, `cannot perform the following tasks:
+- Disconnect consumer:plug from producer:slot (snap "consumer" has no plug named "plug")`)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ slot := repo.Slot("producer", "slot")
+ c.Assert(slot.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ // there is no producer, no slot defined
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "disconnect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, `cannot perform the following tasks:
+- Disconnect consumer:plug from producer:slot (snap "producer" has no slot named "slot")`)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ plug := repo.Plug("consumer", "plug")
+ c.Assert(plug.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestDisconnectPlugFailureNotConnected(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &interfaceAction{
+ Action: "disconnect",
+ Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}},
+ Slots: []slotJSON{{Snap: "producer", Name: "slot"}},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, `cannot perform the following tasks:
+- Disconnect consumer:plug from producer:slot (cannot disconnect consumer:plug from producer:slot, it is not connected)`)
+
+ repo := d.overlord.InterfaceManager().Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, check.HasLen, 0)
+ c.Assert(slot.Connections, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestUnsupportedInterfaceRequest(c *check.C) {
+ buf := bytes.NewBuffer([]byte(`garbage`))
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+}
+
+func (s *apiSuite) TestMissingInterfaceAction(c *check.C) {
+ action := &interfaceAction{}
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "interface action not specified",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+}
+
+func (s *apiSuite) TestUnsupportedInterfaceAction(c *check.C) {
+ s.daemon(c)
+ action := &interfaceAction{Action: "foo"}
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/interfaces", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 400)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "result": map[string]interface{}{
+ "message": "unsupported interface action: \"foo\"",
+ },
+ "status": "Bad Request",
+ "status-code": 400.0,
+ "type": "error",
+ })
+}
+
+func assertAdd(st *state.State, a asserts.Assertion) {
+ st.Lock()
+ defer st.Unlock()
+ err := assertstate.Add(st, a)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func (s *apiSuite) TestAssertOK(c *check.C) {
+ // Setup
+ d := s.daemon(c)
+ st := d.overlord.State()
+ // add store key
+ assertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ buf := bytes.NewBuffer(asserts.Encode(acct))
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rsp := doAssert(assertsCmd, req, nil).(*resp)
+ // Verify (external)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ // Verify (internal)
+ st.Lock()
+ defer st.Unlock()
+ _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{
+ "account-id": acct.AccountID(),
+ })
+ c.Check(err, check.IsNil)
+}
+
+func (s *apiSuite) TestAssertStreamOK(c *check.C) {
+ // Setup
+ d := s.daemon(c)
+ st := d.overlord.State()
+
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ buf := &bytes.Buffer{}
+ enc := asserts.NewEncoder(buf)
+ err := enc.Encode(acct)
+ c.Assert(err, check.IsNil)
+ err = enc.Encode(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, check.IsNil)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rsp := doAssert(assertsCmd, req, nil).(*resp)
+ // Verify (external)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ // Verify (internal)
+ st.Lock()
+ defer st.Unlock()
+ _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{
+ "account-id": acct.AccountID(),
+ })
+ c.Check(err, check.IsNil)
+}
+
+func (s *apiSuite) TestAssertInvalid(c *check.C) {
+ // Setup
+ buf := bytes.NewBufferString("blargh")
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ // Execute
+ assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req)
+ // Verify (external)
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rec.Body.String(), testutil.Contains,
+ "cannot decode request body into assertions")
+}
+
+func (s *apiSuite) TestAssertError(c *check.C) {
+ s.daemon(c)
+ // Setup
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ buf := bytes.NewBuffer(asserts.Encode(acct))
+ req, err := http.NewRequest("POST", "/v2/assertions", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ // Execute
+ assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req)
+ // Verify (external)
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rec.Body.String(), testutil.Contains, "assert failed")
+}
+
+func (s *apiSuite) TestAssertsFindManyAll(c *check.C) {
+ // Setup
+ d := s.daemon(c)
+ // add store key
+ st := d.overlord.State()
+ assertAdd(st, s.storeSigning.StoreAccountKey(""))
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", map[string]interface{}{
+ "account-id": "developer1-id",
+ }, "")
+ assertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account", nil)
+ c.Assert(err, check.IsNil)
+ s.vars = map[string]string{"assertType": "account"}
+ rec := httptest.NewRecorder()
+ assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/x.ubuntu.assertion; bundle=y")
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "3")
+ dec := asserts.NewDecoder(rec.Body)
+ a1, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+ c.Check(a1.Type(), check.Equals, asserts.AccountType)
+
+ a2, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+
+ a3, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+
+ _, err = dec.Decode()
+ c.Assert(err, check.Equals, io.EOF)
+
+ ids := []string{a1.(*asserts.Account).AccountID(), a2.(*asserts.Account).AccountID(), a3.(*asserts.Account).AccountID()}
+ sort.Strings(ids)
+ c.Check(ids, check.DeepEquals, []string{"can0nical", "canonical", "developer1-id"})
+}
+
+func (s *apiSuite) TestAssertsFindManyFilter(c *check.C) {
+ // Setup
+ d := s.daemon(c)
+ // add store key
+ st := d.overlord.State()
+ assertAdd(st, s.storeSigning.StoreAccountKey(""))
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ assertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?username=developer1", nil)
+ c.Assert(err, check.IsNil)
+ s.vars = map[string]string{"assertType": "account"}
+ rec := httptest.NewRecorder()
+ assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "1")
+ dec := asserts.NewDecoder(rec.Body)
+ a1, err := dec.Decode()
+ c.Assert(err, check.IsNil)
+ c.Check(a1.Type(), check.Equals, asserts.AccountType)
+ c.Check(a1.(*asserts.Account).Username(), check.Equals, "developer1")
+ c.Check(a1.(*asserts.Account).AccountID(), check.Equals, acct.AccountID())
+ _, err = dec.Decode()
+ c.Check(err, check.Equals, io.EOF)
+}
+
+func (s *apiSuite) TestAssertsFindManyNoResults(c *check.C) {
+ // Setup
+ d := s.daemon(c)
+ // add store key
+ st := d.overlord.State()
+ assertAdd(st, s.storeSigning.StoreAccountKey(""))
+ acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ assertAdd(st, acct)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/account?username=xyzzyx", nil)
+ c.Assert(err, check.IsNil)
+ s.vars = map[string]string{"assertType": "account"}
+ rec := httptest.NewRecorder()
+ assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf("body %q", rec.Body))
+ c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "0")
+ dec := asserts.NewDecoder(rec.Body)
+ _, err = dec.Decode()
+ c.Check(err, check.Equals, io.EOF)
+}
+
+func (s *apiSuite) TestAssertsInvalidType(c *check.C) {
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/assertions/foo", nil)
+ c.Assert(err, check.IsNil)
+ s.vars = map[string]string{"assertType": "foo"}
+ rec := httptest.NewRecorder()
+ assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req)
+ // Verify
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rec.Body.String(), testutil.Contains, "invalid assert type")
+}
+
+func setupChanges(st *state.State) []string {
+ chg1 := st.NewChange("install", "install...")
+ chg1.Set("snap-names", []string{"funky-snap-name"})
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("activate", "2...")
+ chg1.AddAll(state.NewTaskSet(t1, t2))
+ t1.Logf("l11")
+ t1.Logf("l12")
+ chg2 := st.NewChange("remove", "remove..")
+ t3 := st.NewTask("unlink", "1...")
+ chg2.AddTask(t3)
+ t3.SetStatus(state.ErrorStatus)
+ t3.Errorf("rm failed")
+
+ return []string{chg1.ID(), chg2.ID(), t1.ID(), t2.ID(), t3.ID()}
+}
+
+func (s *apiSuite) TestStateChangesDefaultToInProgress(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ setupChanges(st)
+ st.Unlock()
+
+ // Execute
+ req, err := http.NewRequest("GET", "/v2/changes", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getChanges(stateChangesCmd, req, nil).(*resp)
+
+ // Verify
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Assert(rsp.Result, check.HasLen, 1)
+
+ res, err := rsp.MarshalJSON()
+ c.Assert(err, check.IsNil)
+
+ c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*`)
+}
+
+func (s *apiSuite) TestStateChangesInProgress(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ setupChanges(st)
+ st.Unlock()
+
+ // Execute
+ req, err := http.NewRequest("GET", "/v2/changes?select=in-progress", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getChanges(stateChangesCmd, req, nil).(*resp)
+
+ // Verify
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Assert(rsp.Result, check.HasLen, 1)
+
+ res, err := rsp.MarshalJSON()
+ c.Assert(err, check.IsNil)
+
+ c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`)
+}
+
+func (s *apiSuite) TestStateChangesAll(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ setupChanges(st)
+ st.Unlock()
+
+ // Execute
+ req, err := http.NewRequest("GET", "/v2/changes?select=all", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getChanges(stateChangesCmd, req, nil).(*resp)
+
+ // Verify
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Assert(rsp.Result, check.HasLen, 2)
+
+ res, err := rsp.MarshalJSON()
+ c.Assert(err, check.IsNil)
+
+ c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`)
+ c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`)
+}
+
+func (s *apiSuite) TestStateChangesReady(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ setupChanges(st)
+ st.Unlock()
+
+ // Execute
+ req, err := http.NewRequest("GET", "/v2/changes?select=ready", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getChanges(stateChangesCmd, req, nil).(*resp)
+
+ // Verify
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Assert(rsp.Result, check.HasLen, 1)
+
+ res, err := rsp.MarshalJSON()
+ c.Assert(err, check.IsNil)
+
+ c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`)
+}
+
+func (s *apiSuite) TestStateChangesForSnapName(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ setupChanges(st)
+ st.Unlock()
+
+ // Execute
+ req, err := http.NewRequest("GET", "/v2/changes?for=funky-snap-name&select=all", nil)
+ c.Assert(err, check.IsNil)
+ rsp := getChanges(stateChangesCmd, req, nil).(*resp)
+
+ // Verify
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Assert(rsp.Result, check.FitsTypeOf, []*changeInfo(nil))
+
+ res := rsp.Result.([]*changeInfo)
+ c.Assert(res, check.HasLen, 1)
+ c.Check(res[0].Kind, check.Equals, `install`)
+
+ _, err = rsp.MarshalJSON()
+ c.Assert(err, check.IsNil)
+}
+
+func (s *apiSuite) TestStateChange(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ ids := setupChanges(st)
+ chg := st.Change(ids[0])
+ chg.Set("api-data", map[string]int{"n": 42})
+ st.Unlock()
+ s.vars = map[string]string{"id": ids[0]}
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/change/"+ids[0], nil)
+ c.Assert(err, check.IsNil)
+ rsp := getChange(stateChangeCmd, req, nil).(*resp)
+ rec := httptest.NewRecorder()
+ rsp.ServeHTTP(rec, req)
+
+ // Verify
+ c.Check(rec.Code, check.Equals, 200)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.NotNil)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body["result"], check.DeepEquals, map[string]interface{}{
+ "id": ids[0],
+ "kind": "install",
+ "summary": "install...",
+ "status": "Do",
+ "ready": false,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "tasks": []interface{}{
+ map[string]interface{}{
+ "id": ids[2],
+ "kind": "download",
+ "summary": "1...",
+ "status": "Do",
+ "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"},
+ "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.},
+ "spawn-time": "2016-04-21T01:02:03Z",
+ },
+ map[string]interface{}{
+ "id": ids[3],
+ "kind": "activate",
+ "summary": "2...",
+ "status": "Do",
+ "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.},
+ "spawn-time": "2016-04-21T01:02:03Z",
+ },
+ },
+ "data": map[string]interface{}{
+ "n": float64(42),
+ },
+ })
+}
+
+func (s *apiSuite) TestStateChangeAbort(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ soon := 0
+ ensureStateSoon = func(st *state.State) {
+ soon++
+ }
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ ids := setupChanges(st)
+ st.Unlock()
+ s.vars = map[string]string{"id": ids[0]}
+
+ buf := bytes.NewBufferString(`{"action": "abort"}`)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf)
+ c.Assert(err, check.IsNil)
+ rsp := abortChange(stateChangeCmd, req, nil).(*resp)
+ rec := httptest.NewRecorder()
+ rsp.ServeHTTP(rec, req)
+
+ // Ensure scheduled
+ c.Check(soon, check.Equals, 1)
+
+ // Verify
+ c.Check(rec.Code, check.Equals, 200)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.NotNil)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body["result"], check.DeepEquals, map[string]interface{}{
+ "id": ids[0],
+ "kind": "install",
+ "summary": "install...",
+ "status": "Hold",
+ "ready": true,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:03Z",
+ "tasks": []interface{}{
+ map[string]interface{}{
+ "id": ids[2],
+ "kind": "download",
+ "summary": "1...",
+ "status": "Hold",
+ "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"},
+ "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.},
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:03Z",
+ },
+ map[string]interface{}{
+ "id": ids[3],
+ "kind": "activate",
+ "summary": "2...",
+ "status": "Hold",
+ "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.},
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:03Z",
+ },
+ },
+ })
+}
+
+func (s *apiSuite) TestStateChangeAbortIsReady(c *check.C) {
+ restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
+ defer restore()
+
+ // Setup
+ d := newTestDaemon(c)
+ st := d.overlord.State()
+ st.Lock()
+ ids := setupChanges(st)
+ st.Change(ids[0]).SetStatus(state.DoneStatus)
+ st.Unlock()
+ s.vars = map[string]string{"id": ids[0]}
+
+ buf := bytes.NewBufferString(`{"action": "abort"}`)
+
+ // Execute
+ req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf)
+ c.Assert(err, check.IsNil)
+ rsp := abortChange(stateChangeCmd, req, nil).(*resp)
+ rec := httptest.NewRecorder()
+ rsp.ServeHTTP(rec, req)
+
+ // Verify
+ c.Check(rec.Code, check.Equals, 400)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result, check.NotNil)
+
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ c.Check(body["result"], check.DeepEquals, map[string]interface{}{
+ "message": fmt.Sprintf("cannot abort change %s with nothing pending", ids[0]),
+ })
+}
+
+const validBuyInput = `{
+ "snap-id": "the-snap-id-1234abcd",
+ "snap-name": "the snap name",
+ "price": 1.23,
+ "currency": "EUR"
+ }`
+
+var validBuyOptions = &store.BuyOptions{
+ SnapID: "the-snap-id-1234abcd",
+ Price: 1.23,
+ Currency: "EUR",
+}
+
+var buyTests = []struct {
+ input string
+ result *store.BuyResult
+ err error
+ expectedStatus int
+ expectedResult interface{}
+ expectedResponseType ResponseType
+ expectedBuyOptions *store.BuyOptions
+}{
+ {
+ // Success
+ input: validBuyInput,
+ result: &store.BuyResult{
+ State: "Complete",
+ },
+ expectedStatus: http.StatusOK,
+ expectedResult: &store.BuyResult{
+ State: "Complete",
+ },
+ expectedResponseType: ResponseTypeSync,
+ expectedBuyOptions: validBuyOptions,
+ },
+ {
+ // Fail with internal error
+ input: `{
+ "snap-id": "the-snap-id-1234abcd",
+ "price": 1.23,
+ "currency": "EUR"
+ }`,
+ err: fmt.Errorf("internal error banana"),
+ expectedStatus: http.StatusInternalServerError,
+ expectedResponseType: ResponseTypeError,
+ expectedResult: &errorResult{
+ Message: "internal error banana",
+ },
+ expectedBuyOptions: &store.BuyOptions{
+ SnapID: "the-snap-id-1234abcd",
+ Price: 1.23,
+ Currency: "EUR",
+ },
+ },
+ {
+ // Fail with unauthenticated error
+ input: validBuyInput,
+ err: store.ErrUnauthenticated,
+ expectedStatus: http.StatusBadRequest,
+ expectedResponseType: ResponseTypeError,
+ expectedResult: &errorResult{
+ Message: "you need to log in first",
+ Kind: "login-required",
+ },
+ expectedBuyOptions: validBuyOptions,
+ },
+ {
+ // Fail with TOS not accepted
+ input: validBuyInput,
+ err: store.ErrTOSNotAccepted,
+ expectedStatus: http.StatusBadRequest,
+ expectedResponseType: ResponseTypeError,
+ expectedResult: &errorResult{
+ Message: "terms of service not accepted",
+ Kind: "terms-not-accepted",
+ },
+ expectedBuyOptions: validBuyOptions,
+ },
+ {
+ // Fail with no payment methods
+ input: validBuyInput,
+ err: store.ErrNoPaymentMethods,
+ expectedStatus: http.StatusBadRequest,
+ expectedResponseType: ResponseTypeError,
+ expectedResult: &errorResult{
+ Message: "no payment methods",
+ Kind: "no-payment-methods",
+ },
+ expectedBuyOptions: validBuyOptions,
+ },
+ {
+ // Fail with payment declined
+ input: validBuyInput,
+ err: store.ErrPaymentDeclined,
+ expectedStatus: http.StatusBadRequest,
+ expectedResponseType: ResponseTypeError,
+ expectedResult: &errorResult{
+ Message: "payment declined",
+ Kind: "payment-declined",
+ },
+ expectedBuyOptions: validBuyOptions,
+ },
+}
+
+func (s *apiSuite) TestBuySnap(c *check.C) {
+ for _, test := range buyTests {
+ s.buyResult = test.result
+ s.err = test.err
+
+ buf := bytes.NewBufferString(test.input)
+ req, err := http.NewRequest("POST", "/v2/buy", buf)
+ c.Assert(err, check.IsNil)
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ rsp := postBuy(buyCmd, req, user).(*resp)
+
+ c.Check(rsp.Status, check.Equals, test.expectedStatus)
+ c.Check(rsp.Type, check.Equals, test.expectedResponseType)
+ c.Assert(rsp.Result, check.FitsTypeOf, test.expectedResult)
+ c.Check(rsp.Result, check.DeepEquals, test.expectedResult)
+
+ c.Check(s.buyOptions, check.DeepEquals, test.expectedBuyOptions)
+ c.Check(s.user, check.Equals, user)
+ }
+}
+
+func (s *apiSuite) TestIsTrue(c *check.C) {
+ form := &multipart.Form{}
+ c.Check(isTrue(form, "foo"), check.Equals, false)
+ for _, f := range []string{"", "false", "0", "False", "f", "try"} {
+ form.Value = map[string][]string{"foo": {f}}
+ c.Check(isTrue(form, "foo"), check.Equals, false, check.Commentf("expected %q to be false", f))
+ }
+ for _, t := range []string{"true", "1", "True", "t"} {
+ form.Value = map[string][]string{"foo": {t}}
+ c.Check(isTrue(form, "foo"), check.Equals, true, check.Commentf("expected %q to be true", t))
+ }
+}
+
+var readyToBuyTests = []struct {
+ input error
+ status int
+ respType interface{}
+ response interface{}
+}{
+ {
+ // Success
+ input: nil,
+ status: http.StatusOK,
+ respType: ResponseTypeSync,
+ response: true,
+ },
+ {
+ // Not accepted TOS
+ input: store.ErrTOSNotAccepted,
+ status: http.StatusBadRequest,
+ respType: ResponseTypeError,
+ response: &errorResult{
+ Message: "terms of service not accepted",
+ Kind: errorKindTermsNotAccepted,
+ },
+ },
+ {
+ // No payment methods
+ input: store.ErrNoPaymentMethods,
+ status: http.StatusBadRequest,
+ respType: ResponseTypeError,
+ response: &errorResult{
+ Message: "no payment methods",
+ Kind: errorKindNoPaymentMethods,
+ },
+ },
+}
+
+func (s *apiSuite) TestReadyToBuy(c *check.C) {
+ for _, test := range readyToBuyTests {
+ s.err = test.input
+
+ req, err := http.NewRequest("GET", "/v2/buy/ready", nil)
+ c.Assert(err, check.IsNil)
+
+ state := snapCmd.d.overlord.State()
+ state.Lock()
+ user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ state.Unlock()
+ c.Check(err, check.IsNil)
+
+ rsp := readyToBuy(readyToBuyCmd, req, user).(*resp)
+ c.Check(rsp.Status, check.Equals, test.status)
+ c.Check(rsp.Type, check.Equals, test.respType)
+ c.Assert(rsp.Result, check.FitsTypeOf, test.response)
+ c.Check(rsp.Result, check.DeepEquals, test.response)
+ }
+}
+
+var _ = check.Suite(&postCreateUserSuite{})
+
+type postCreateUserSuite struct {
+ apiBaseSuite
+
+ mockUserHome string
+}
+
+func (s *postCreateUserSuite) SetUpTest(c *check.C) {
+ s.apiBaseSuite.SetUpTest(c)
+
+ s.daemon(c)
+ postCreateUserUcrednetGetUID = func(string) (uint32, error) {
+ return 0, nil
+ }
+ s.mockUserHome = c.MkDir()
+ userLookup = mkUserLookup(s.mockUserHome)
+}
+
+func (s *postCreateUserSuite) TearDownTest(c *check.C) {
+ s.apiBaseSuite.TearDownTest(c)
+
+ postCreateUserUcrednetGetUID = ucrednetGetUID
+ userLookup = user.Lookup
+ osutilAddUser = osutil.AddUser
+ storeUserInfo = store.UserInfo
+}
+
+func mkUserLookup(userHomeDir string) func(string) (*user.User, error) {
+ return func(username string) (*user.User, error) {
+ cur, err := user.Current()
+ cur.Username = username
+ cur.HomeDir = userHomeDir
+ return cur, err
+ }
+}
+
+func (s *postCreateUserSuite) TestPostCreateUserNoSSHKeys(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ storeUserInfo = func(user string) (*store.User, error) {
+ c.Check(user, check.Equals, "popper@lse.ac.uk")
+ return &store.User{
+ Username: "karl",
+ OpenIDIdentifier: "xxyyzz",
+ }, nil
+ }
+
+ buf := bytes.NewBufferString(`{"email": "popper@lse.ac.uk"}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user for "popper@lse.ac.uk": no ssh keys found`)
+}
+
+func (s *postCreateUserSuite) TestPostCreateUser(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ storeUserInfo = func(user string) (*store.User, error) {
+ c.Check(user, check.Equals, "popper@lse.ac.uk")
+ return &store.User{
+ Username: "karl",
+ SSHKeys: []string{"ssh1", "ssh2"},
+ OpenIDIdentifier: "xxyyzz",
+ }, nil
+ }
+ osutilAddUser = func(username string, opts *osutil.AddUserOptions) error {
+ c.Check(username, check.Equals, "karl")
+ c.Check(opts.SSHKeys, check.DeepEquals, []string{"ssh1", "ssh2"})
+ c.Check(opts.Gecos, check.Equals, "popper@lse.ac.uk,xxyyzz")
+ c.Check(opts.Sudoer, check.Equals, false)
+ return nil
+ }
+
+ buf := bytes.NewBufferString(`{"email": "popper@lse.ac.uk"}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ expected := &userResponseData{
+ Username: "karl",
+ SSHKeys: []string{"ssh1", "ssh2"},
+ }
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+
+ // user was setup in state
+ state := s.d.overlord.State()
+ state.Lock()
+ user, err := auth.User(state, 1)
+ state.Unlock()
+ c.Check(err, check.IsNil)
+ c.Check(user.Username, check.Equals, "karl")
+ c.Check(user.Email, check.Equals, "popper@lse.ac.uk")
+ c.Check(user.Macaroon, check.NotNil)
+ // auth saved to user home dir
+ outfile := filepath.Join(s.mockUserHome, ".snap", "auth.json")
+ c.Check(osutil.FileExists(outfile), check.Equals, true)
+ content, err := ioutil.ReadFile(outfile)
+ c.Check(err, check.IsNil)
+ c.Check(string(content), check.Equals, fmt.Sprintf(`{"macaroon":"%s"}`, user.Macaroon))
+}
+
+func (s *postCreateUserSuite) TestGetUserDetailsFromAssertionModelNotFound(c *check.C) {
+ st := s.d.overlord.State()
+ email := "foo@example.com"
+
+ username, opts, err := getUserDetailsFromAssertion(st, email)
+ c.Check(username, check.Equals, "")
+ c.Check(opts, check.IsNil)
+ c.Check(err, check.ErrorMatches, `cannot add system-user "foo@example.com": cannot get model assertion: no state entry for key`)
+}
+
+func (s *postCreateUserSuite) setupSigner(accountID string, signerPrivKey asserts.PrivateKey) *assertstest.SigningDB {
+ st := s.d.overlord.State()
+
+ // create fake brand signature
+ signerSigning := assertstest.NewSigningDB(accountID, signerPrivKey)
+
+ signerAcct := assertstest.NewAccount(s.storeSigning, accountID, map[string]interface{}{
+ "account-id": accountID,
+ "verification": "certified",
+ }, "")
+ s.storeSigning.Add(signerAcct)
+ assertAdd(st, signerAcct)
+
+ signerAccKey := assertstest.NewAccountKey(s.storeSigning, signerAcct, nil, signerPrivKey.PublicKey(), "")
+ s.storeSigning.Add(signerAccKey)
+ assertAdd(st, signerAccKey)
+
+ return signerSigning
+}
+
+var (
+ brandPrivKey, _ = assertstest.GenerateKey(752)
+ partnerPrivKey, _ = assertstest.GenerateKey(752)
+ unknownPrivKey, _ = assertstest.GenerateKey(752)
+)
+
+func (s *postCreateUserSuite) makeSystemUsers(c *check.C, systemUsers []map[string]interface{}) {
+ st := s.d.overlord.State()
+
+ assertAdd(st, s.storeSigning.StoreAccountKey(""))
+
+ brandSigning := s.setupSigner("my-brand", brandPrivKey)
+ partnerSigning := s.setupSigner("partner", partnerPrivKey)
+ unknownSigning := s.setupSigner("unknown", unknownPrivKey)
+
+ signers := map[string]*assertstest.SigningDB{
+ "my-brand": brandSigning,
+ "partner": partnerSigning,
+ "unknown": unknownSigning,
+ }
+
+ model, err := brandSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "architecture": "amd64",
+ "gadget": "pc",
+ "kernel": "pc-kernel",
+ "required-snaps": []interface{}{"required-snap1"},
+ "system-user-authority": []interface{}{"my-brand", "partner"},
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ model = model.(*asserts.Model)
+
+ // now add model related stuff to the system
+ assertAdd(st, model)
+
+ for _, suMap := range systemUsers {
+ su, err := signers[suMap["authority-id"].(string)].Sign(asserts.SystemUserType, suMap, nil, "")
+ c.Assert(err, check.IsNil)
+ su = su.(*asserts.SystemUser)
+ // now add system-user assertion to the system
+ assertAdd(st, su)
+ }
+ // create fake device
+ st.Lock()
+ err = auth.SetDevice(st, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ })
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+}
+
+var goodUser = map[string]interface{}{
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "email": "foo@bar.com",
+ "series": []interface{}{"16", "18"},
+ "models": []interface{}{"my-model", "other-model"},
+ "name": "Boring Guy",
+ "username": "guy",
+ "password": "$6$salt$hash",
+ "since": time.Now().Format(time.RFC3339),
+ "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
+}
+
+var partnerUser = map[string]interface{}{
+ "authority-id": "partner",
+ "brand-id": "my-brand",
+ "email": "p@partner.com",
+ "series": []interface{}{"16", "18"},
+ "models": []interface{}{"my-model"},
+ "name": "Partner Guy",
+ "username": "partnerguy",
+ "password": "$6$salt$hash",
+ "since": time.Now().Format(time.RFC3339),
+ "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
+}
+
+var badUser = map[string]interface{}{
+ // bad user (not valid for this model)
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "email": "foobar@bar.com",
+ "series": []interface{}{"16", "18"},
+ "models": []interface{}{"non-of-the-models-i-have"},
+ "name": "Random Gal",
+ "username": "gal",
+ "password": "$6$salt$hash",
+ "since": time.Now().Format(time.RFC3339),
+ "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
+}
+
+var unknownUser = map[string]interface{}{
+ "authority-id": "unknown",
+ "brand-id": "my-brand",
+ "email": "x@partner.com",
+ "series": []interface{}{"16", "18"},
+ "models": []interface{}{"my-model"},
+ "name": "XGuy",
+ "username": "xguy",
+ "password": "$6$salt$hash",
+ "since": time.Now().Format(time.RFC3339),
+ "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339),
+}
+
+func (s *postCreateUserSuite) TestGetUserDetailsFromAssertionHappy(c *check.C) {
+ s.makeSystemUsers(c, []map[string]interface{}{goodUser})
+
+ // ensure that if we query the details from the assert DB we get
+ // the expected user
+ st := s.d.overlord.State()
+ username, opts, err := getUserDetailsFromAssertion(st, "foo@bar.com")
+ c.Check(username, check.Equals, "guy")
+ c.Check(opts, check.DeepEquals, &osutil.AddUserOptions{
+ Gecos: "foo@bar.com,Boring Guy",
+ Password: "$6$salt$hash",
+ })
+ c.Check(err, check.IsNil)
+}
+
+// FIXME: These tests all look similar, with small deltas. Would be
+// nice to transform them into a table that is just the deltas, and
+// run on a loop.
+func (s *postCreateUserSuite) TestPostCreateUserFromAssertion(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ s.makeSystemUsers(c, []map[string]interface{}{goodUser})
+
+ // mock the calls that create the user
+ osutilAddUser = func(username string, opts *osutil.AddUserOptions) error {
+ c.Check(username, check.Equals, "guy")
+ c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy")
+ c.Check(opts.Sudoer, check.Equals, false)
+ c.Check(opts.Password, check.Equals, "$6$salt$hash")
+ return nil
+ }
+
+ defer func() {
+ osutilAddUser = osutil.AddUser
+ }()
+
+ // do it!
+ buf := bytes.NewBufferString(`{"email": "foo@bar.com","known":true}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ expected := &userResponseData{
+ Username: "guy",
+ }
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+
+ // ensure the user was added to the state
+ st := s.d.overlord.State()
+ st.Lock()
+ users, err := auth.Users(st)
+ c.Assert(err, check.IsNil)
+ st.Unlock()
+ c.Check(users, check.HasLen, 1)
+}
+
+func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, badUser, unknownUser})
+
+ // mock the calls that create the user
+ osutilAddUser = func(username string, opts *osutil.AddUserOptions) error {
+ switch username {
+ case "guy":
+ c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy")
+ case "partnerguy":
+ c.Check(opts.Gecos, check.Equals, "p@partner.com,Partner Guy")
+ default:
+ c.Logf("unexpected username %q", username)
+ c.Fail()
+ }
+ c.Check(opts.Sudoer, check.Equals, false)
+ c.Check(opts.Password, check.Equals, "$6$salt$hash")
+ return nil
+ }
+ defer func() {
+ osutilAddUser = osutil.AddUser
+ }()
+
+ // do it!
+ buf := bytes.NewBufferString(`{"known":true}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ // note that we get a list here instead of a single
+ // userResponseData item
+ c.Check(rsp.Result, check.FitsTypeOf, []userResponseData{})
+ seen := map[string]bool{}
+ for _, u := range rsp.Result.([]userResponseData) {
+ seen[u.Username] = true
+ c.Check(u, check.DeepEquals, userResponseData{Username: u.Username})
+ }
+ c.Check(seen, check.DeepEquals, map[string]bool{
+ "guy": true,
+ "partnerguy": true,
+ })
+
+ // ensure the user was added to the state
+ st := s.d.overlord.State()
+ st.Lock()
+ users, err := auth.Users(st)
+ c.Assert(err, check.IsNil)
+ st.Unlock()
+ c.Check(users, check.HasLen, 2)
+}
+
+func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownClassicErrors(c *check.C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ s.makeSystemUsers(c, []map[string]interface{}{goodUser})
+
+ postCreateUserUcrednetGetUID = func(string) (uint32, error) {
+ return 0, nil
+ }
+ defer func() {
+ postCreateUserUcrednetGetUID = ucrednetGetUID
+ }()
+
+ // do it!
+ buf := bytes.NewBufferString(`{"known":true}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device is a classic system`)
+}
+
+func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownButOwnedErrors(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ s.makeSystemUsers(c, []map[string]interface{}{goodUser})
+
+ st := s.d.overlord.State()
+ st.Lock()
+ _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"})
+ st.Unlock()
+ c.Check(err, check.IsNil)
+
+ // do it!
+ buf := bytes.NewBufferString(`{"known":true}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device already managed`)
+}
+
+func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownButOwned(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ s.makeSystemUsers(c, []map[string]interface{}{goodUser})
+
+ st := s.d.overlord.State()
+ st.Lock()
+ _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"})
+ st.Unlock()
+ c.Check(err, check.IsNil)
+
+ // mock the calls that create the user
+ osutilAddUser = func(username string, opts *osutil.AddUserOptions) error {
+ c.Check(username, check.Equals, "guy")
+ c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy")
+ c.Check(opts.Sudoer, check.Equals, false)
+ c.Check(opts.Password, check.Equals, "$6$salt$hash")
+ return nil
+ }
+ defer func() {
+ osutilAddUser = osutil.AddUser
+ }()
+
+ // do it!
+ buf := bytes.NewBufferString(`{"known":true,"force-managed":true}`)
+ req, err := http.NewRequest("POST", "/v2/create-user", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := postCreateUser(createUserCmd, req, nil).(*resp)
+
+ // note that we get a list here instead of a single
+ // userResponseData item
+ expected := []userResponseData{
+ {Username: "guy"},
+ }
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+}
+
+func (s *postCreateUserSuite) TestUsersEmpty(c *check.C) {
+ req, err := http.NewRequest("GET", "/v2/users", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getUsers(usersCmd, req, nil).(*resp)
+
+ expected := []userResponseData{}
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+}
+
+func (s *postCreateUserSuite) TestUsersHasUser(c *check.C) {
+ st := s.d.overlord.State()
+ st.Lock()
+ u, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"})
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+
+ req, err := http.NewRequest("GET", "/v2/users", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getUsers(usersCmd, req, nil).(*resp)
+
+ expected := []userResponseData{
+ {ID: u.ID, Username: u.Username, Email: u.Email},
+ }
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result, check.FitsTypeOf, expected)
+ c.Check(rsp.Result, check.DeepEquals, expected)
+}
+
+func (s *postCreateUserSuite) TestSysinfoIsManaged(c *check.C) {
+ st := s.d.overlord.State()
+ st.Lock()
+ _, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"})
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+
+ req, err := http.NewRequest("GET", "/v2/system-info", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := sysInfo(sysInfoCmd, req, nil).(*resp)
+
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Result.(map[string]interface{})["managed"], check.Equals, true)
+}
+
+// aliases
+
+func (s *apiSuite) TestAliasSuccess(c *check.C) {
+ err := os.MkdirAll(dirs.SnapBinariesDir, 0755)
+ c.Assert(err, check.IsNil)
+ d := s.daemon(c)
+
+ s.mockSnap(c, aliasYaml)
+
+ oldAutoAliases := snapstate.AutoAliases
+ snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) {
+ return nil, nil
+ }
+ defer func() { snapstate.AutoAliases = oldAutoAliases }()
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &aliasAction{
+ Action: "alias",
+ Snap: "alias-snap",
+ Aliases: []string{"alias1"},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/aliases", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ err = chg.Err()
+ st.Unlock()
+ c.Assert(err, check.IsNil)
+
+ // sanity check
+ c.Check(osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, true)
+}
+
+func (s *apiSuite) TestAliasErrors(c *check.C) {
+ s.daemon(c)
+
+ errScenarios := []struct {
+ mangle func(*aliasAction)
+ err string
+ }{
+ {func(a *aliasAction) { a.Action = "" }, `unsupported alias action: ""`},
+ {func(a *aliasAction) { a.Action = "what" }, `unsupported alias action: "what"`},
+ {func(a *aliasAction) { a.Aliases = nil }, `at least one alias name is required`},
+ {func(a *aliasAction) { a.Snap = "lalala" }, `cannot find snap "lalala"`},
+ }
+
+ for _, scen := range errScenarios {
+ action := &aliasAction{
+ Action: "alias",
+ Snap: "alias-snap",
+ Aliases: []string{"alias1"},
+ }
+ scen.mangle(action)
+
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/aliases", buf)
+ c.Assert(err, check.IsNil)
+
+ rsp := changeAliases(aliasesCmd, req, nil).(*resp)
+ c.Check(rsp.Type, check.Equals, ResponseTypeError)
+ c.Check(rsp.Status, check.Equals, http.StatusBadRequest)
+ c.Check(rsp.Result.(*errorResult).Message, check.Matches, scen.err)
+ }
+}
+
+func (s *apiSuite) TestUnaliasSuccess(c *check.C) {
+ err := os.MkdirAll(dirs.SnapBinariesDir, 0755)
+ c.Assert(err, check.IsNil)
+ d := s.daemon(c)
+
+ s.mockSnap(c, aliasYaml)
+
+ oldAutoAliases := snapstate.AutoAliases
+ snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) {
+ return nil, nil
+ }
+ defer func() { snapstate.AutoAliases = oldAutoAliases }()
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ action := &aliasAction{
+ Action: "unalias",
+ Snap: "alias-snap",
+ Aliases: []string{"alias1"},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/aliases", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ st := d.overlord.State()
+ st.Lock()
+ chg := st.Change(id)
+ st.Unlock()
+ c.Assert(chg, check.NotNil)
+
+ <-chg.Ready()
+
+ st.Lock()
+ defer st.Unlock()
+ err = chg.Err()
+ c.Assert(err, check.IsNil)
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, check.IsNil)
+ c.Check(allAliases, check.DeepEquals, map[string]map[string]string{
+ "alias-snap": {"alias1": "disabled"},
+ })
+
+}
+
+func (s *apiSuite) TestResetAliasSuccess(c *check.C) {
+ err := os.MkdirAll(dirs.SnapBinariesDir, 0755)
+ c.Assert(err, check.IsNil)
+ d := s.daemon(c)
+
+ s.mockSnap(c, aliasYaml)
+
+ oldAutoAliases := snapstate.AutoAliases
+ snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) {
+ return nil, nil
+ }
+ defer func() { snapstate.AutoAliases = oldAutoAliases }()
+
+ d.overlord.Loop()
+ defer d.overlord.Stop()
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ st.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "disabled",
+ },
+ })
+
+ action := &aliasAction{
+ Action: "reset",
+ Snap: "alias-snap",
+ Aliases: []string{"alias1"},
+ }
+ text, err := json.Marshal(action)
+ c.Assert(err, check.IsNil)
+ buf := bytes.NewBuffer(text)
+ req, err := http.NewRequest("POST", "/v2/aliases", buf)
+ c.Assert(err, check.IsNil)
+ rec := httptest.NewRecorder()
+ st.Unlock()
+ aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req)
+ st.Lock()
+ c.Check(rec.Code, check.Equals, 202)
+ var body map[string]interface{}
+ err = json.Unmarshal(rec.Body.Bytes(), &body)
+ c.Check(err, check.IsNil)
+ id := body["change"].(string)
+
+ chg := st.Change(id)
+ c.Assert(chg, check.NotNil)
+
+ st.Unlock()
+ <-chg.Ready()
+ st.Lock()
+
+ err = chg.Err()
+ c.Assert(err, check.IsNil)
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, check.IsNil)
+ c.Check(allAliases, check.HasLen, 0)
+}
+
+func (s *apiSuite) TestAliases(c *check.C) {
+ d := s.daemon(c)
+
+ s.mockSnap(c, aliasYaml)
+
+ st := d.overlord.State()
+ st.Lock()
+ st.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias3": "disabled", // gone from the current revision of the snap
+ },
+ })
+ st.Unlock()
+
+ req, err := http.NewRequest("GET", "/v2/aliases", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := getAliases(aliasesCmd, req, nil).(*resp)
+ c.Check(rsp.Type, check.Equals, ResponseTypeSync)
+ c.Check(rsp.Status, check.Equals, http.StatusOK)
+ c.Check(rsp.Result, check.DeepEquals, map[string]map[string]aliasStatus{
+ "alias-snap": {
+ "alias1": {App: "alias-snap.app", Status: "enabled"},
+ "alias2": {App: "alias-snap.app2"},
+ "alias3": {Status: "disabled"},
+ },
+ })
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "runtime"
+ "strings"
+ unix "syscall"
+ "time"
+
+ "github.com/coreos/go-systemd/activation"
+ "github.com/gorilla/mux"
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/store"
+)
+
+// A Daemon listens for requests and routes them to the right command
+type Daemon struct {
+ Version string
+ overlord *overlord.Overlord
+ snapdListener net.Listener
+ snapListener net.Listener
+ tomb tomb.Tomb
+ router *mux.Router
+ // enableInternalInterfaceActions controls if adding and removing slots and plugs is allowed.
+ enableInternalInterfaceActions bool
+}
+
+// A ResponseFunc handles one of the individual verbs for a method
+type ResponseFunc func(*Command, *http.Request, *auth.UserState) Response
+
+// A Command routes a request to an individual per-verb ResponseFUnc
+type Command struct {
+ Path string
+ //
+ GET ResponseFunc
+ PUT ResponseFunc
+ POST ResponseFunc
+ DELETE ResponseFunc
+ // can guest GET?
+ GuestOK bool
+ // can non-admin GET?
+ UserOK bool
+ // is this path accessible on the snapd-snap socket?
+ SnapOK bool
+
+ d *Daemon
+}
+
+func (c *Command) canAccess(r *http.Request, user *auth.UserState) bool {
+ if user != nil {
+ // Authenticated users do anything for now.
+ return true
+ }
+
+ isUser := false
+ uid, err := ucrednetGetUID(r.RemoteAddr)
+ if err == nil {
+ if uid == 0 {
+ // Superuser does anything.
+ return true
+ }
+
+ isUser = true
+ } else if err != errNoUID {
+ logger.Noticef("unexpected error when attempting to get UID: %s", err)
+ return false
+ } else if c.SnapOK {
+ return true
+ }
+
+ if r.Method != "GET" {
+ return false
+ }
+
+ if isUser && c.UserOK {
+ return true
+ }
+
+ if c.GuestOK {
+ return true
+ }
+
+ return false
+}
+
+func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ state := c.d.overlord.State()
+ state.Lock()
+ // TODO Look at the error and fail if there's an attempt to authenticate with invalid data.
+ user, _ := UserFromRequest(state, r)
+ state.Unlock()
+
+ if !c.canAccess(r, user) {
+ Unauthorized("access denied").ServeHTTP(w, r)
+ return
+ }
+
+ var rspf ResponseFunc
+ var rsp = BadMethod("method %q not allowed", r.Method)
+
+ switch r.Method {
+ case "GET":
+ rspf = c.GET
+ case "PUT":
+ rspf = c.PUT
+ case "POST":
+ rspf = c.POST
+ case "DELETE":
+ rspf = c.DELETE
+ }
+
+ if rspf != nil {
+ rsp = rspf(c, r, user)
+ }
+
+ rsp.ServeHTTP(w, r)
+}
+
+type wrappedWriter struct {
+ w http.ResponseWriter
+ s int
+}
+
+func (w *wrappedWriter) Header() http.Header {
+ return w.w.Header()
+}
+
+func (w *wrappedWriter) Write(bs []byte) (int, error) {
+ return w.w.Write(bs)
+}
+
+func (w *wrappedWriter) WriteHeader(s int) {
+ w.w.WriteHeader(s)
+ w.s = s
+}
+
+func logit(handler http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ww := &wrappedWriter{w: w}
+ t0 := time.Now()
+ handler.ServeHTTP(ww, r)
+ t := time.Now().Sub(t0)
+ url := r.URL.String()
+ if !strings.Contains(url, "/changes/") {
+ logger.Debugf("%s %s %s %s %d", r.RemoteAddr, r.Method, r.URL, t, ww.s)
+ }
+ })
+}
+
+// getListener tries to get a listener for the given socket path from
+// the listener map, and if it fails it tries to set it up directly.
+func getListener(socketPath string, listenerMap map[string]net.Listener) (net.Listener, error) {
+ if listener, ok := listenerMap[socketPath]; ok {
+ return listener, nil
+ }
+
+ if c, err := net.Dial("unix", socketPath); err == nil {
+ c.Close()
+ return nil, fmt.Errorf("socket %q already in use", socketPath)
+ }
+
+ if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
+ return nil, err
+ }
+
+ address, err := net.ResolveUnixAddr("unix", socketPath)
+ if err != nil {
+ return nil, err
+ }
+
+ runtime.LockOSThread()
+ oldmask := unix.Umask(0111)
+ listener, err := net.ListenUnix("unix", address)
+ unix.Umask(oldmask)
+ runtime.UnlockOSThread()
+ if err != nil {
+ return nil, err
+ }
+
+ logger.Debugf("socket %q was not activated; listening", socketPath)
+
+ return listener, nil
+}
+
+// Init sets up the Daemon's internal workings.
+// Don't call more than once.
+func (d *Daemon) Init() error {
+ t0 := time.Now()
+ listeners, err := activation.Listeners(false)
+ if err != nil {
+ return err
+ }
+
+ listenerMap := make(map[string]net.Listener, len(listeners))
+
+ for _, listener := range listeners {
+ listenerMap[listener.Addr().String()] = listener
+ }
+
+ // The SnapdSocket is required-- without it, die.
+ if listener, err := getListener(dirs.SnapdSocket, listenerMap); err == nil {
+ d.snapdListener = &ucrednetListener{listener}
+ } else {
+ return fmt.Errorf("when trying to listen on %s: %v", dirs.SnapdSocket, err)
+ }
+
+ if listener, err := getListener(dirs.SnapSocket, listenerMap); err == nil {
+ // Note that the SnapSocket listener does not use ucrednet. We use the lack
+ // of remote information as an indication that the request originated with
+ // this socket. This listener may also be nil if that socket wasn't among
+ // the listeners, so check it before using it.
+ d.snapListener = listener
+ } else {
+ logger.Debugf("cannot get listener for %q: %v", dirs.SnapSocket, err)
+ }
+
+ d.addRoutes()
+
+ logger.Debugf("init done in %s", time.Now().Sub(t0))
+ logger.Noticef("started %v.", store.UserAgent())
+
+ return nil
+}
+
+func (d *Daemon) addRoutes() {
+ d.router = mux.NewRouter()
+
+ for _, c := range api {
+ c.d = d
+ d.router.Handle(c.Path, c).Name(c.Path)
+ }
+
+ // also maybe add a /favicon.ico handler...
+
+ d.router.NotFoundHandler = NotFound("not found")
+}
+
+var shutdownMsg = i18n.G("reboot scheduled to update the system - temporarily cancel with 'sudo shutdown -c'")
+
+// Start the Daemon
+func (d *Daemon) Start() {
+ // die when asked to restart (systemd should get us back up!)
+ d.overlord.SetRestartHandler(func(t state.RestartType) {
+ switch t {
+ case state.RestartDaemon:
+ d.tomb.Kill(nil)
+ case state.RestartSystem:
+ cmd := exec.Command("shutdown", "+10", "-r", shutdownMsg)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ logger.Noticef("%s", osutil.OutputErr(out, err))
+ }
+ default:
+ logger.Noticef("internal error: restart handler called with unknown restart type: %v", t)
+ d.tomb.Kill(nil)
+ }
+ })
+
+ // the loop runs in its own goroutine
+ d.overlord.Loop()
+
+ d.tomb.Go(func() error {
+ if d.snapListener != nil {
+ d.tomb.Go(func() error {
+ if err := http.Serve(d.snapListener, logit(d.router)); err != nil && d.tomb.Err() == tomb.ErrStillAlive {
+ return err
+ }
+
+ return nil
+ })
+ }
+
+ if err := http.Serve(d.snapdListener, logit(d.router)); err != nil && d.tomb.Err() == tomb.ErrStillAlive {
+ return err
+ }
+
+ return nil
+ })
+}
+
+// Stop shuts down the Daemon
+func (d *Daemon) Stop() error {
+ d.tomb.Kill(nil)
+ d.snapdListener.Close()
+ if d.snapListener != nil {
+ d.snapListener.Close()
+ }
+ d.overlord.Stop()
+
+ return d.tomb.Wait()
+}
+
+// Dying is a tomb-ish thing
+func (d *Daemon) Dying() <-chan struct{} {
+ return d.tomb.Dying()
+}
+
+// New Daemon
+func New() (*Daemon, error) {
+ ovld, err := overlord.New()
+ if err != nil {
+ return nil, err
+ }
+ return &Daemon{
+ overlord: ovld,
+ // TODO: Decide when this should be disabled by default.
+ enableInternalInterfaceActions: true,
+ }, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/gorilla/mux"
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { check.TestingT(t) }
+
+type daemonSuite struct{}
+
+var _ = check.Suite(&daemonSuite{})
+
+func (s *daemonSuite) SetUpTest(c *check.C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, check.IsNil)
+}
+
+func (s *daemonSuite) TearDownTest(c *check.C) {
+ dirs.SetRootDir("")
+}
+
+// build a new daemon, with only a little of Init(), suitable for the tests
+func newTestDaemon(c *check.C) *Daemon {
+ d, err := New()
+ c.Assert(err, check.IsNil)
+ d.addRoutes()
+
+ return d
+}
+
+// a Response suitable for testing
+type mockHandler struct {
+ cmd *Command
+ lastMethod string
+}
+
+func (mck *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ mck.lastMethod = r.Method
+}
+
+func mkRF(c *check.C, cmd *Command, mck *mockHandler) ResponseFunc {
+ return func(innerCmd *Command, req *http.Request, user *auth.UserState) Response {
+ c.Assert(cmd, check.Equals, innerCmd)
+ return mck
+ }
+}
+
+func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) {
+ cmd := &Command{d: newTestDaemon(c)}
+ mck := &mockHandler{cmd: cmd}
+ rf := mkRF(c, cmd, mck)
+ cmd.GET = rf
+ cmd.PUT = rf
+ cmd.POST = rf
+ cmd.DELETE = rf
+
+ for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {
+ req, err := http.NewRequest(method, "", nil)
+ c.Assert(err, check.IsNil)
+
+ rec := httptest.NewRecorder()
+ cmd.ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, http.StatusUnauthorized, check.Commentf(method))
+
+ rec = httptest.NewRecorder()
+ req.RemoteAddr = "uid=0;" + req.RemoteAddr
+
+ cmd.ServeHTTP(rec, req)
+ c.Check(mck.lastMethod, check.Equals, method)
+ c.Check(rec.Code, check.Equals, http.StatusOK)
+ }
+
+ req, err := http.NewRequest("POTATO", "", nil)
+ c.Assert(err, check.IsNil)
+ req.RemoteAddr = "uid=0;" + req.RemoteAddr
+
+ rec := httptest.NewRecorder()
+ cmd.ServeHTTP(rec, req)
+ c.Check(rec.Code, check.Equals, http.StatusMethodNotAllowed)
+}
+
+func (s *daemonSuite) TestGuestAccess(c *check.C) {
+ get := &http.Request{Method: "GET"}
+ put := &http.Request{Method: "PUT"}
+ pst := &http.Request{Method: "POST"}
+ del := &http.Request{Method: "DELETE"}
+
+ cmd := &Command{d: newTestDaemon(c)}
+ c.Check(cmd.canAccess(get, nil), check.Equals, false)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+ c.Check(cmd.canAccess(pst, nil), check.Equals, false)
+ c.Check(cmd.canAccess(del, nil), check.Equals, false)
+
+ cmd = &Command{d: newTestDaemon(c), UserOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, false)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+ c.Check(cmd.canAccess(pst, nil), check.Equals, false)
+ c.Check(cmd.canAccess(del, nil), check.Equals, false)
+
+ cmd = &Command{d: newTestDaemon(c), GuestOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+ c.Check(cmd.canAccess(pst, nil), check.Equals, false)
+ c.Check(cmd.canAccess(del, nil), check.Equals, false)
+
+ // Since this request has no RemoteAddr, it must be coming from the snap
+ // socket instead of the snapd one. In that case, if SnapOK is true, this
+ // command should be wide open for all HTTP methods.
+ cmd = &Command{d: newTestDaemon(c), SnapOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, true)
+ c.Check(cmd.canAccess(pst, nil), check.Equals, true)
+ c.Check(cmd.canAccess(del, nil), check.Equals, true)
+}
+
+func (s *daemonSuite) TestUserAccess(c *check.C) {
+ get := &http.Request{Method: "GET", RemoteAddr: "uid=42;"}
+ put := &http.Request{Method: "PUT", RemoteAddr: "uid=42;"}
+
+ cmd := &Command{d: newTestDaemon(c)}
+ c.Check(cmd.canAccess(get, nil), check.Equals, false)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+
+ cmd = &Command{d: newTestDaemon(c), UserOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+
+ cmd = &Command{d: newTestDaemon(c), GuestOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+
+ // Since this request has a RemoteAddr, it must be coming from the snapd
+ // socket instead of the snap one. In that case, SnapOK should have no
+ // bearing on the default behavior, which is to deny access.
+ cmd = &Command{d: newTestDaemon(c), SnapOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, false)
+ c.Check(cmd.canAccess(put, nil), check.Equals, false)
+}
+
+func (s *daemonSuite) TestSuperAccess(c *check.C) {
+ get := &http.Request{Method: "GET", RemoteAddr: "uid=0;"}
+ put := &http.Request{Method: "PUT", RemoteAddr: "uid=0;"}
+
+ cmd := &Command{d: newTestDaemon(c)}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, true)
+
+ cmd = &Command{d: newTestDaemon(c), UserOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, true)
+
+ cmd = &Command{d: newTestDaemon(c), GuestOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, true)
+
+ cmd = &Command{d: newTestDaemon(c), SnapOK: true}
+ c.Check(cmd.canAccess(get, nil), check.Equals, true)
+ c.Check(cmd.canAccess(put, nil), check.Equals, true)
+}
+
+func (s *daemonSuite) TestAddRoutes(c *check.C) {
+ d := newTestDaemon(c)
+
+ expected := make([]string, len(api))
+ for i, v := range api {
+ expected[i] = v.Path
+ }
+
+ got := make([]string, 0, len(api))
+ c.Assert(d.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
+ got = append(got, route.GetName())
+ return nil
+ }), check.IsNil)
+
+ c.Check(got, check.DeepEquals, expected) // this'll stop being true if routes are added that aren't commands (e.g. for the favicon)
+
+ // XXX: still waiting to know how to check d.router.NotFoundHandler has been set to NotFound
+ // the old test relied on undefined behaviour:
+ // c.Check(fmt.Sprintf("%p", d.router.NotFoundHandler), check.Equals, fmt.Sprintf("%p", NotFound))
+}
+
+type witnessAcceptListener struct {
+ net.Listener
+ accept chan struct{}
+}
+
+func (l *witnessAcceptListener) Accept() (net.Conn, error) {
+ close(l.accept)
+ return l.Listener.Accept()
+}
+
+func (s *daemonSuite) TestStartStop(c *check.C) {
+ d := newTestDaemon(c)
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ c.Assert(err, check.IsNil)
+
+ snapdAccept := make(chan struct{})
+ d.snapdListener = &witnessAcceptListener{l, snapdAccept}
+
+ snapAccept := make(chan struct{})
+ d.snapListener = &witnessAcceptListener{l, snapAccept}
+
+ d.Start()
+
+ snapdDone := make(chan struct{})
+ go func() {
+ select {
+ case <-snapdAccept:
+ case <-time.After(2 * time.Second):
+ c.Fatal("snapd accept was not called")
+ }
+ close(snapdDone)
+ }()
+
+ snapDone := make(chan struct{})
+ go func() {
+ select {
+ case <-snapAccept:
+ case <-time.After(2 * time.Second):
+ c.Fatal("snapd accept was not called")
+ }
+ close(snapDone)
+ }()
+
+ <-snapdDone
+ <-snapDone
+
+ err = d.Stop()
+ c.Check(err, check.IsNil)
+}
+
+func (s *daemonSuite) TestRestartWiring(c *check.C) {
+ d := newTestDaemon(c)
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ c.Assert(err, check.IsNil)
+
+ snapdAccept := make(chan struct{})
+ d.snapdListener = &witnessAcceptListener{l, snapdAccept}
+
+ snapAccept := make(chan struct{})
+ d.snapListener = &witnessAcceptListener{l, snapAccept}
+
+ d.Start()
+ defer d.Stop()
+
+ snapdDone := make(chan struct{})
+ go func() {
+ select {
+ case <-snapdAccept:
+ case <-time.After(2 * time.Second):
+ c.Fatal("snapd accept was not called")
+ }
+ close(snapdDone)
+ }()
+
+ snapDone := make(chan struct{})
+ go func() {
+ select {
+ case <-snapAccept:
+ case <-time.After(2 * time.Second):
+ c.Fatal("snap accept was not called")
+ }
+ close(snapDone)
+ }()
+
+ <-snapdDone
+ <-snapDone
+
+ d.overlord.State().RequestRestart(state.RestartDaemon)
+
+ select {
+ case <-d.Dying():
+ case <-time.After(2 * time.Second):
+ c.Fatal("RequestRestart -> overlord -> Kill chain didn't work")
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "encoding/json"
+ "fmt"
+ "mime"
+ "net/http"
+ "path/filepath"
+ "strconv"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/logger"
+)
+
+// ResponseType is the response type
+type ResponseType string
+
+// "there are three standard return types: Standard return value,
+// Background operation, Error", each returning a JSON object with the
+// following "type" field:
+const (
+ ResponseTypeSync ResponseType = "sync"
+ ResponseTypeAsync ResponseType = "async"
+ ResponseTypeError ResponseType = "error"
+)
+
+// Response knows how to serve itself, and how to find itself
+type Response interface {
+ ServeHTTP(w http.ResponseWriter, r *http.Request)
+}
+
+type resp struct {
+ Status int `json:"status-code"`
+ Type ResponseType `json:"type"`
+ Result interface{} `json:"result"`
+ *Meta
+}
+
+// TODO This is being done in a rush to get the proper external
+// JSON representation in the API in time for the release.
+// The right code style takes a bit more work and unifies
+// these fields inside resp.
+type Meta struct {
+ Sources []string `json:"sources,omitempty"`
+ Paging *Paging `json:"paging,omitempty"`
+ SuggestedCurrency string `json:"suggested-currency,omitempty"`
+ Change string `json:"change,omitempty"`
+}
+
+type Paging struct {
+ Page int `json:"page"`
+ Pages int `json:"pages"`
+}
+
+type respJSON struct {
+ Type ResponseType `json:"type"`
+ Status int `json:"status-code"`
+ StatusText string `json:"status"`
+ Result interface{} `json:"result"`
+ *Meta
+}
+
+func (r *resp) MarshalJSON() ([]byte, error) {
+ return json.Marshal(respJSON{
+ Type: r.Type,
+ Status: r.Status,
+ StatusText: http.StatusText(r.Status),
+ Result: r.Result,
+ Meta: r.Meta,
+ })
+}
+
+func (r *resp) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
+ status := r.Status
+ bs, err := r.MarshalJSON()
+ if err != nil {
+ logger.Noticef("cannot marshal %#v to JSON: %v", *r, err)
+ bs = nil
+ status = http.StatusInternalServerError
+ }
+
+ hdr := w.Header()
+ if r.Status == http.StatusAccepted || r.Status == http.StatusCreated {
+ if m, ok := r.Result.(map[string]interface{}); ok {
+ if location, ok := m["resource"]; ok {
+ if location, ok := location.(string); ok && location != "" {
+ hdr.Set("Location", location)
+ }
+ }
+ }
+ }
+
+ hdr.Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ w.Write(bs)
+}
+
+type errorKind string
+
+const (
+ errorKindTwoFactorRequired = errorKind("two-factor-required")
+ errorKindTwoFactorFailed = errorKind("two-factor-failed")
+ errorKindLoginRequired = errorKind("login-required")
+ errorKindInvalidAuthData = errorKind("invalid-auth-data")
+ errorKindTermsNotAccepted = errorKind("terms-not-accepted")
+ errorKindNoPaymentMethods = errorKind("no-payment-methods")
+ errorKindPaymentDeclined = errorKind("payment-declined")
+
+ errorKindSnapAlreadyInstalled = errorKind("snap-already-installed")
+ errorKindSnapNotInstalled = errorKind("snap-not-installed")
+ errorKindSnapNoUpdateAvailable = errorKind("snap-no-update-available")
+)
+
+type errorValue interface{}
+
+type errorResult struct {
+ Message string `json:"message"` // note no omitempty
+ Kind errorKind `json:"kind,omitempty"`
+ Value errorValue `json:"value,omitempty"`
+}
+
+// SyncResponse builds a "sync" response from the given result.
+func SyncResponse(result interface{}, meta *Meta) Response {
+ if err, ok := result.(error); ok {
+ return InternalError("internal error: %v", err)
+ }
+
+ if rsp, ok := result.(Response); ok {
+ return rsp
+ }
+
+ return &resp{
+ Type: ResponseTypeSync,
+ Status: http.StatusOK,
+ Result: result,
+ Meta: meta,
+ }
+}
+
+// AsyncResponse builds an "async" response from the given *Task
+func AsyncResponse(result map[string]interface{}, meta *Meta) Response {
+ return &resp{
+ Type: ResponseTypeAsync,
+ Status: http.StatusAccepted,
+ Result: result,
+ Meta: meta,
+ }
+}
+
+// makeErrorResponder builds an errorResponder from the given error status.
+func makeErrorResponder(status int) errorResponder {
+ return func(format string, v ...interface{}) Response {
+ res := &errorResult{
+ Message: fmt.Sprintf(format, v...),
+ }
+ if status == http.StatusUnauthorized {
+ res.Kind = errorKindLoginRequired
+ }
+ return &resp{
+ Type: ResponseTypeError,
+ Result: res,
+ Status: status,
+ }
+ }
+}
+
+// A FileResponse 's ServeHTTP method serves the file
+type FileResponse string
+
+// ServeHTTP from the Response interface
+func (f FileResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ filename := fmt.Sprintf("attachment; filename=%s", filepath.Base(string(f)))
+ w.Header().Add("Content-Disposition", filename)
+ http.ServeFile(w, r, string(f))
+}
+
+type assertResponse struct {
+ assertions []asserts.Assertion
+ bundle bool
+}
+
+// AssertResponse builds a response whose ServerHTTP method serves one or a bundle of assertions.
+func AssertResponse(asserts []asserts.Assertion, bundle bool) Response {
+ if len(asserts) > 1 {
+ bundle = true
+ }
+ return &assertResponse{assertions: asserts, bundle: bundle}
+}
+
+func (ar assertResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ t := asserts.MediaType
+ if ar.bundle {
+ t = mime.FormatMediaType(t, map[string]string{"bundle": "y"})
+ }
+ w.Header().Set("Content-Type", t)
+ w.Header().Set("X-Ubuntu-Assertions-Count", strconv.Itoa(len(ar.assertions)))
+ w.WriteHeader(http.StatusOK)
+ enc := asserts.NewEncoder(w)
+ for _, a := range ar.assertions {
+ err := enc.Encode(a)
+ if err != nil {
+ logger.Noticef("cannot write encoded assertion into response: %v", err)
+ break
+
+ }
+ }
+}
+
+// errorResponder is a callable that produces an error Response.
+// e.g., InternalError("something broke: %v", err), etc.
+type errorResponder func(string, ...interface{}) Response
+
+// standard error responses
+var (
+ Unauthorized = makeErrorResponder(http.StatusUnauthorized)
+ NotFound = makeErrorResponder(http.StatusNotFound)
+ BadRequest = makeErrorResponder(http.StatusBadRequest)
+ BadMethod = makeErrorResponder(http.StatusMethodNotAllowed)
+ InternalError = makeErrorResponder(http.StatusInternalServerError)
+ NotImplemented = makeErrorResponder(http.StatusNotImplemented)
+ Forbidden = makeErrorResponder(http.StatusForbidden)
+ Conflict = makeErrorResponder(http.StatusConflict)
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/check.v1"
+)
+
+type responseSuite struct{}
+
+var _ = check.Suite(&responseSuite{})
+
+func (s *responseSuite) TestRespSetsLocationIfAccepted(c *check.C) {
+ rec := httptest.NewRecorder()
+
+ rsp := &resp{
+ Status: http.StatusAccepted,
+ Result: map[string]interface{}{
+ "resource": "foo/bar",
+ },
+ }
+
+ rsp.ServeHTTP(rec, nil)
+ hdr := rec.Header()
+ c.Check(hdr.Get("Location"), check.Equals, "foo/bar")
+}
+
+func (s *responseSuite) TestRespSetsLocationIfCreated(c *check.C) {
+ rec := httptest.NewRecorder()
+
+ rsp := &resp{
+ Status: http.StatusCreated,
+ Result: map[string]interface{}{
+ "resource": "foo/bar",
+ },
+ }
+
+ rsp.ServeHTTP(rec, nil)
+ hdr := rec.Header()
+ c.Check(hdr.Get("Location"), check.Equals, "foo/bar")
+}
+
+func (s *responseSuite) TestRespDoesNotSetLocationIfOther(c *check.C) {
+ rec := httptest.NewRecorder()
+
+ rsp := &resp{
+ Status: http.StatusTeapot,
+ Result: map[string]interface{}{
+ "resource": "foo/bar",
+ },
+ }
+
+ rsp.ServeHTTP(rec, nil)
+ hdr := rec.Header()
+ c.Check(hdr.Get("Location"), check.Equals, "")
+}
+
+func (s *responseSuite) TestFileResponseSetsContentDisposition(c *check.C) {
+ const filename = "icon.png"
+
+ path := filepath.Join(c.MkDir(), filename)
+ err := ioutil.WriteFile(path, nil, os.ModePerm)
+ c.Check(err, check.IsNil)
+
+ rec := httptest.NewRecorder()
+ rsp := FileResponse(path)
+ req, err := http.NewRequest("GET", "", nil)
+ c.Check(err, check.IsNil)
+
+ rsp.ServeHTTP(rec, req)
+
+ hdr := rec.Header()
+ c.Check(hdr.Get("Content-Disposition"), check.Equals,
+ fmt.Sprintf("attachment; filename=%s", filename))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+var errNoSnap = errors.New("no snap installed")
+
+// snapIcon tries to find the icon inside the snap
+func snapIcon(info *snap.Info) string {
+ // XXX: copy of snap.Snap.Icon which will go away
+ found, _ := filepath.Glob(filepath.Join(info.MountDir(), "meta", "gui", "icon.*"))
+ if len(found) == 0 {
+ return info.IconURL
+ }
+
+ return found[0]
+}
+
+// snapDate returns the time of the snap mount directory.
+func snapDate(info *snap.Info) time.Time {
+ st, err := os.Stat(info.MountDir())
+ if err != nil {
+ return time.Time{}
+ }
+
+ return st.ModTime()
+}
+
+func publisherName(st *state.State, info *snap.Info) (string, error) {
+ if info.SnapID == "" {
+ return "", nil
+ }
+
+ pubAcct, err := assertstate.Publisher(st, info.SnapID)
+ if err != nil {
+ return "", fmt.Errorf("cannot find publisher details: %v", err)
+ }
+ return pubAcct.Username(), nil
+}
+
+type aboutSnap struct {
+ info *snap.Info
+ snapst *snapstate.SnapState
+ publisher string
+}
+
+// localSnapInfo returns the information about the current snap for the given name plus the SnapState with the active flag and other snap revisions.
+func localSnapInfo(st *state.State, name string) (aboutSnap, error) {
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return aboutSnap{}, fmt.Errorf("cannot consult state: %v", err)
+ }
+
+ info, err := snapst.CurrentInfo()
+ if err == snapstate.ErrNoCurrent {
+ return aboutSnap{}, errNoSnap
+ }
+ if err != nil {
+ return aboutSnap{}, fmt.Errorf("cannot read snap details: %v", err)
+ }
+
+ publisher, err := publisherName(st, info)
+ if err != nil {
+ return aboutSnap{}, err
+ }
+
+ return aboutSnap{
+ info: info,
+ snapst: &snapst,
+ publisher: publisher,
+ }, nil
+}
+
+// allLocalSnapInfos returns the information about the all current snaps and their SnapStates.
+func allLocalSnapInfos(st *state.State, all bool) ([]aboutSnap, error) {
+ st.Lock()
+ defer st.Unlock()
+
+ snapStates, err := snapstate.All(st)
+ if err != nil {
+ return nil, err
+ }
+ about := make([]aboutSnap, 0, len(snapStates))
+
+ var firstErr error
+ for _, snapst := range snapStates {
+ var aboutThis []aboutSnap
+ var info *snap.Info
+ var publisher string
+ var err error
+ if all {
+ for _, seq := range snapst.Sequence {
+ info, err = snap.ReadInfo(seq.RealName, seq)
+ if err != nil {
+ break
+ }
+ publisher, err = publisherName(st, info)
+ aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher})
+ }
+ } else {
+ info, err = snapst.CurrentInfo()
+ if err == nil {
+ var publisher string
+ publisher, err = publisherName(st, info)
+ aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher})
+ }
+ }
+
+ if err != nil {
+ // XXX: aggregate instead?
+ if firstErr == nil {
+ firstErr = err
+ }
+ continue
+ }
+ about = append(about, aboutThis...)
+ }
+
+ return about, firstErr
+}
+
+// appJSON contains the json for snap.AppInfo
+type appJSON struct {
+ Name string `json:"name"`
+ Daemon string `json:"daemon"`
+ Aliases []string `json:"aliases"`
+}
+
+// screenshotJSON contains the json for snap.ScreenshotInfo
+type screenshotJSON struct {
+ URL string `json:"url"`
+ Width int64 `json:"width,omitempty"`
+ Height int64 `json:"height,omitempty"`
+}
+
+func mapLocal(about aboutSnap) map[string]interface{} {
+ localSnap, snapst := about.info, about.snapst
+ status := "installed"
+ if snapst.Active && localSnap.Revision == snapst.Current {
+ status = "active"
+ }
+
+ apps := make([]appJSON, 0, len(localSnap.Apps))
+ for _, app := range localSnap.Apps {
+ apps = append(apps, appJSON{
+ Name: app.Name,
+ Daemon: app.Daemon,
+ Aliases: app.Aliases,
+ })
+ }
+
+ return map[string]interface{}{
+ "description": localSnap.Description(),
+ "developer": about.publisher,
+ "icon": snapIcon(localSnap),
+ "id": localSnap.SnapID,
+ "install-date": snapDate(localSnap),
+ "installed-size": localSnap.Size,
+ "name": localSnap.Name(),
+ "revision": localSnap.Revision,
+ "status": status,
+ "summary": localSnap.Summary(),
+ "type": string(localSnap.Type),
+ "version": localSnap.Version,
+ "channel": localSnap.Channel,
+ "tracking-channel": snapst.Channel,
+ "confinement": localSnap.Confinement,
+ "devmode": snapst.DevMode,
+ "trymode": snapst.TryMode,
+ "jailmode": snapst.JailMode,
+ "private": localSnap.Private,
+ "apps": apps,
+ "broken": localSnap.Broken,
+ }
+}
+
+func mapRemote(remoteSnap *snap.Info) map[string]interface{} {
+ status := "available"
+ if remoteSnap.MustBuy {
+ status = "priced"
+ }
+
+ confinement := remoteSnap.Confinement
+ if confinement == "" {
+ confinement = snap.StrictConfinement
+ }
+
+ screenshots := make([]screenshotJSON, len(remoteSnap.Screenshots))
+ for i, screenshot := range remoteSnap.Screenshots {
+ screenshots[i] = screenshotJSON{
+ URL: screenshot.URL,
+ Width: screenshot.Width,
+ Height: screenshot.Height,
+ }
+ }
+
+ result := map[string]interface{}{
+ "description": remoteSnap.Description(),
+ "developer": remoteSnap.Publisher,
+ "download-size": remoteSnap.Size,
+ "icon": snapIcon(remoteSnap),
+ "id": remoteSnap.SnapID,
+ "name": remoteSnap.Name(),
+ "revision": remoteSnap.Revision,
+ "status": status,
+ "summary": remoteSnap.Summary(),
+ "type": string(remoteSnap.Type),
+ "version": remoteSnap.Version,
+ "channel": remoteSnap.Channel,
+ "private": remoteSnap.Private,
+ "confinement": confinement,
+ }
+
+ if len(screenshots) > 0 {
+ result["screenshots"] = screenshots
+ }
+
+ if len(remoteSnap.Prices) > 0 {
+ result["prices"] = remoteSnap.Prices
+ }
+
+ if len(remoteSnap.Channels) > 0 {
+ result["channels"] = remoteSnap.Channels
+ }
+
+ return result
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+ sys "syscall"
+)
+
+var errNoUID = errors.New("no uid found")
+
+const ucrednetNobody = uint32((1 << 32) - 1)
+
+func ucrednetGetUID(remoteAddr string) (uint32, error) {
+ idx := strings.IndexByte(remoteAddr, ';')
+ if !strings.HasPrefix(remoteAddr, "uid=") || idx < 5 {
+ return ucrednetNobody, errNoUID
+ }
+
+ uid, err := strconv.ParseUint(remoteAddr[4:idx], 10, 32)
+ if err != nil {
+ return ucrednetNobody, err
+ }
+
+ return uint32(uid), nil
+}
+
+type ucrednetAddr struct {
+ net.Addr
+ uid string
+}
+
+func (wa *ucrednetAddr) String() string {
+ return fmt.Sprintf("uid=%s;%s", wa.uid, wa.Addr)
+}
+
+type ucrednetConn struct {
+ net.Conn
+ uid string
+}
+
+func (wc *ucrednetConn) RemoteAddr() net.Addr {
+ return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.uid}
+}
+
+type ucrednetListener struct{ net.Listener }
+
+var getUcred = sys.GetsockoptUcred
+
+func (wl *ucrednetListener) Accept() (net.Conn, error) {
+ con, err := wl.Listener.Accept()
+ if err != nil {
+ return nil, err
+ }
+
+ uid := ""
+ if ucon, ok := con.(*net.UnixConn); ok {
+ f, err := ucon.File()
+ if err != nil {
+ return nil, err
+ }
+ // File() is a dup(); needs closing
+ defer f.Close()
+
+ ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED)
+ if err != nil {
+ return nil, err
+ }
+
+ uid = strconv.FormatUint(uint64(ucred.Uid), 10)
+ }
+
+ return &ucrednetConn{con, uid}, err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package daemon
+
+import (
+ "errors"
+ "net"
+ "path/filepath"
+ sys "syscall"
+
+ "gopkg.in/check.v1"
+)
+
+type ucrednetSuite struct {
+ ucred *sys.Ucred
+ err error
+}
+
+var _ = check.Suite(&ucrednetSuite{})
+
+func (s *ucrednetSuite) getUcred(fd, level, opt int) (*sys.Ucred, error) {
+ return s.ucred, s.err
+}
+
+func (s *ucrednetSuite) SetUpSuite(c *check.C) {
+ getUcred = s.getUcred
+}
+
+func (s *ucrednetSuite) TearDownTest(c *check.C) {
+ s.ucred = nil
+ s.err = nil
+}
+func (s *ucrednetSuite) TearDownSuite(c *check.C) {
+ getUcred = sys.GetsockoptUcred
+}
+
+func (s *ucrednetSuite) TestAcceptConnRemoteAddrString(c *check.C) {
+ s.ucred = &sys.Ucred{Uid: 42}
+ d := c.MkDir()
+ sock := filepath.Join(d, "sock")
+
+ l, err := net.Listen("unix", sock)
+ c.Assert(err, check.IsNil)
+ defer l.Close()
+
+ go func() {
+ cli, err := net.Dial("unix", sock)
+ c.Assert(err, check.IsNil)
+ cli.Close()
+ }()
+
+ wl := &ucrednetListener{l}
+
+ conn, err := wl.Accept()
+ c.Assert(err, check.IsNil)
+ defer conn.Close()
+
+ remoteAddr := conn.RemoteAddr().String()
+ c.Check(remoteAddr, check.Matches, "uid=42;.*")
+ uid, err := ucrednetGetUID(remoteAddr)
+ c.Check(uid, check.Equals, uint32(42))
+ c.Check(err, check.IsNil)
+}
+
+func (s *ucrednetSuite) TestNonUnix(c *check.C) {
+ l, err := net.Listen("tcp", "localhost:0")
+ c.Assert(err, check.IsNil)
+ defer l.Close()
+
+ addr := l.Addr().String()
+
+ go func() {
+ cli, err := net.Dial("tcp", addr)
+ c.Assert(err, check.IsNil)
+ cli.Close()
+ }()
+
+ wl := &ucrednetListener{l}
+
+ conn, err := wl.Accept()
+ c.Assert(err, check.IsNil)
+ defer conn.Close()
+
+ remoteAddr := conn.RemoteAddr().String()
+ c.Check(remoteAddr, check.Matches, "uid=;.*")
+ uid, err := ucrednetGetUID(remoteAddr)
+ c.Check(uid, check.Equals, ucrednetNobody)
+ c.Check(err, check.Equals, errNoUID)
+}
+
+func (s *ucrednetSuite) TestAcceptErrors(c *check.C) {
+ s.ucred = &sys.Ucred{Uid: 42}
+ d := c.MkDir()
+ sock := filepath.Join(d, "sock")
+
+ l, err := net.Listen("unix", sock)
+ c.Assert(err, check.IsNil)
+ c.Assert(l.Close(), check.IsNil)
+
+ wl := &ucrednetListener{l}
+
+ _, err = wl.Accept()
+ c.Assert(err, check.NotNil)
+}
+
+func (s *ucrednetSuite) TestUcredErrors(c *check.C) {
+ s.err = errors.New("oopsie")
+ d := c.MkDir()
+ sock := filepath.Join(d, "sock")
+
+ l, err := net.Listen("unix", sock)
+ c.Assert(err, check.IsNil)
+ defer l.Close()
+
+ go func() {
+ cli, err := net.Dial("unix", sock)
+ c.Assert(err, check.IsNil)
+ cli.Close()
+ }()
+
+ wl := &ucrednetListener{l}
+
+ _, err = wl.Accept()
+ c.Assert(err, check.Equals, s.err)
+}
+
+func (s *ucrednetSuite) TestGetNoUid(c *check.C) {
+ uid, err := ucrednetGetUID("uid=;")
+ c.Check(err, check.Equals, errNoUID)
+ c.Check(uid, check.Equals, ucrednetNobody)
+}
+
+func (s *ucrednetSuite) TestGetBadUid(c *check.C) {
+ uid, err := ucrednetGetUID("uid=hello;")
+ c.Check(err, check.NotNil)
+ c.Check(uid, check.Equals, ucrednetNobody)
+}
+
+func (s *ucrednetSuite) TestGetNonUcrednet(c *check.C) {
+ uid, err := ucrednetGetUID("hello")
+ c.Check(err, check.Equals, errNoUID)
+ c.Check(uid, check.Equals, ucrednetNobody)
+}
+
+func (s *ucrednetSuite) TestGetNothing(c *check.C) {
+ uid, err := ucrednetGetUID("")
+ c.Check(err, check.Equals, errNoUID)
+ c.Check(uid, check.Equals, ucrednetNobody)
+}
+
+func (s *ucrednetSuite) TestGet(c *check.C) {
+ uid, err := ucrednetGetUID("uid=42;")
+ c.Check(err, check.IsNil)
+ c.Check(uid, check.Equals, uint32(42))
+}
--- /dev/null
+# -*- sh -*-
+_complete() {
+ local cur prev words cword
+ _init_completion || return
+
+ local command
+ if [[ ${#words[@]} -gt 2 ]]; then
+ if [[ ${words[1]} =~ ^-- ]]; then
+ # global options take no args
+ return 0
+ fi
+ if [[ ${words[-2]} = "--help" ]]; then
+ # help takes no args
+ return 0
+ fi
+
+ command=${words[-2]}
+ fi
+
+ # Only split on newlines
+ local IFS=$'\n'
+
+ COMPREPLY=($(GO_FLAGS_COMPLETION=1 "${words[@]}"))
+
+ case $command in
+ install|info|sign-build)
+ _filedir "snap"
+ ;;
+ ack)
+ _filedir
+ ;;
+ try)
+ _filedir -d
+ ;;
+ esac
+
+ return 0
+}
+
+complete -F _complete snap
--- /dev/null
+\e[38;5;226m\e[48;5;88m░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░█▀▀░█▀▄░█░█░█▀▀░█░█░▀█▀░█▀█░█▀▀░░░█▀▀░█▀█░▀█▀░█░░░█░█░█▀▄░█▀▀░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░█░░░█▀▄░█░█░▀▀█░█▀█░░█░░█░█░█░█░░░█▀▀░█▀█░░█░░█░░░█░█░█▀▄░█▀▀░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░▀▀▀░▀░▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀░▀░▀▀▀░░░▀░░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░░░░░░░░░░░█▀█░█▀█░█▀▄░░░█▀▄░█▀▀░█▀▀░█▀█░█▀█░▀█▀░█▀▄░░░░░░░░░░░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░░░░░░░░░░░█▀█░█░█░█░█░░░█░█░█▀▀░▀▀█░█▀▀░█▀█░░█░░█▀▄░░░░░░░░░░░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░░░░░░░░░░░▀░▀░▀░▀░▀▀░░░░▀▀░░▀▀▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀░▀░░░░░░░░░░░░░\e[0m
+\e[38;5;226m\e[48;5;88m░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\e[0m
--- /dev/null
+VERSION=unknown
--- /dev/null
+ \e[0;1;36;96m▒\e[0;1;34;94m██\e[0;1;35;95m▒\e[0m \e[0;1;33;93m█\e[0;1;32;92m██\e[0;1;36;96m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m██\e[0;1;32;92m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m█\e[0m
+ \e[0;1;34;94m▓\e[0;1;35;95m██\e[0;1;31;91m▓\e[0m \e[0;1;32;92m█\e[0;1;36;96m██\e[0;1;34;94m█\e[0m \e[0;1;33;93m█\e[0;1;32;92m██\e[0;1;36;96m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m█\e[0m
+ \e[0;1;35;95m█\e[0;1;31;91m██\e[0;1;33;93m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m█\e[0m \e[0;1;36;96m█\e[0;1;34;94m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m█\e[0m
+ \e[0;1;31;91m█\e[0;1;33;93m██\e[0;1;32;92m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m█\e[0m \e[0;1;32;92m▒█\e[0;1;36;96m██\e[0;1;34;94m▒█\e[0;1;35;95m█\e[0m \e[0;1;33;93m░█\e[0;1;32;92m██\e[0;1;36;96m█░\e[0m \e[0;1;31;91m░█\e[0;1;33;93m██\e[0;1;32;92m█░\e[0m \e[0;1;35;95m▒█\e[0;1;31;91m██\e[0;1;33;93m░█\e[0;1;32;92m█\e[0m
+ \e[0;1;33;93m▒█\e[0;1;32;92m▓▓\e[0;1;36;96m█▒\e[0m \e[0;1;31;91m█\e[0;1;33;93m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m█\e[0m \e[0;1;32;92m░\e[0;1;36;96m██\e[0;1;34;94m██\e[0;1;35;95m██\e[0;1;31;91m█\e[0m \e[0;1;33;93m░\e[0;1;32;92m██\e[0;1;36;96m██\e[0;1;34;94m██\e[0;1;35;95m░\e[0m \e[0;1;31;91m░\e[0;1;33;93m██\e[0;1;32;92m██\e[0;1;36;96m██\e[0;1;34;94m░\e[0m \e[0;1;35;95m▒\e[0;1;31;91m██\e[0;1;33;93m██\e[0;1;32;92m██\e[0;1;36;96m█\e[0m
+ \e[0;1;32;92m▓█\e[0;1;36;96m▒▒\e[0;1;34;94m█▓\e[0m \e[0;1;33;93m█\e[0;1;32;92m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m█\e[0m \e[0;1;36;96m█\e[0;1;34;94m██\e[0m \e[0;1;31;91m██\e[0;1;33;93m█\e[0m \e[0;1;32;92m█\e[0;1;36;96m██\e[0m \e[0;1;35;95m██\e[0;1;31;91m█\e[0m \e[0;1;33;93m█\e[0;1;32;92m██\e[0m \e[0;1;34;94m██\e[0;1;35;95m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m██\e[0m \e[0;1;36;96m██\e[0;1;34;94m█\e[0m
+ \e[0;1;36;96m██\e[0m \e[0;1;35;95m██\e[0m \e[0;1;32;92m█\e[0;1;36;96m█\e[0m \e[0;1;33;93m█\e[0;1;32;92m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m█░\e[0m \e[0;1;33;93m░█\e[0;1;32;92m█\e[0m \e[0;1;36;96m█\e[0;1;34;94m█░\e[0m \e[0;1;31;91m░█\e[0;1;33;93m█\e[0m \e[0;1;32;92m█\e[0;1;36;96m█░\e[0m \e[0;1;35;95m░█\e[0;1;31;91m█\e[0m \e[0;1;33;93m█\e[0;1;32;92m█░\e[0m \e[0;1;34;94m░█\e[0;1;35;95m█\e[0m
+ \e[0;1;34;94m██\e[0;1;35;95m██\e[0;1;31;91m██\e[0m \e[0;1;36;96m█\e[0;1;34;94m█\e[0m \e[0;1;32;92m█\e[0;1;36;96m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m█\e[0m \e[0;1;32;92m█\e[0;1;36;96m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m█\e[0m \e[0;1;33;93m█\e[0;1;32;92m█\e[0m \e[0;1;36;96m█\e[0;1;34;94m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m█\e[0m \e[0;1;32;92m█\e[0;1;36;96m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m█\e[0m
+ \e[0;1;34;94m░\e[0;1;35;95m██\e[0;1;31;91m██\e[0;1;33;93m██\e[0;1;32;92m░\e[0m \e[0;1;34;94m█\e[0;1;35;95m█\e[0m \e[0;1;36;96m█\e[0;1;34;94m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m█░\e[0m \e[0;1;36;96m░█\e[0;1;34;94m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m█░\e[0m \e[0;1;32;92m░█\e[0;1;36;96m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m█░\e[0m \e[0;1;33;93m░█\e[0;1;32;92m█\e[0m \e[0;1;36;96m█\e[0;1;34;94m█░\e[0m \e[0;1;31;91m░█\e[0;1;33;93m█\e[0m
+ \e[0;1;35;95m▒\e[0;1;31;91m██\e[0m \e[0;1;32;92m██\e[0;1;36;96m▒\e[0m \e[0;1;35;95m█\e[0;1;31;91m█▒\e[0m \e[0;1;34;94m█\e[0;1;35;95m█▒\e[0m \e[0;1;33;93m█\e[0;1;32;92m██\e[0m \e[0;1;34;94m██\e[0;1;35;95m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m██\e[0m \e[0;1;36;96m██\e[0;1;34;94m█\e[0m \e[0;1;35;95m█\e[0;1;31;91m██\e[0m \e[0;1;32;92m██\e[0;1;36;96m█\e[0m \e[0;1;34;94m█\e[0;1;35;95m██\e[0m \e[0;1;33;93m██\e[0;1;32;92m█\e[0m
+ \e[0;1;31;91m█\e[0;1;33;93m██\e[0m \e[0;1;36;96m██\e[0;1;34;94m█\e[0m \e[0;1;31;91m█\e[0;1;33;93m██\e[0;1;32;92m██\e[0m \e[0;1;35;95m█\e[0;1;31;91m██\e[0;1;33;93m██\e[0m \e[0;1;32;92m░\e[0;1;36;96m██\e[0;1;34;94m██\e[0;1;35;95m██\e[0;1;31;91m█\e[0m \e[0;1;33;93m░\e[0;1;32;92m██\e[0;1;36;96m██\e[0;1;34;94m██\e[0;1;35;95m░\e[0m \e[0;1;31;91m░\e[0;1;33;93m██\e[0;1;32;92m██\e[0;1;36;96m██\e[0;1;34;94m░\e[0m \e[0;1;35;95m▒\e[0;1;31;91m██\e[0;1;33;93m██\e[0;1;32;92m██\e[0;1;36;96m█\e[0m
+ \e[0;1;33;93m█\e[0;1;32;92m█▒\e[0m \e[0;1;34;94m▒█\e[0;1;35;95m█\e[0m \e[0;1;33;93m░\e[0;1;32;92m██\e[0;1;36;96m██\e[0m \e[0;1;31;91m░\e[0;1;33;93m██\e[0;1;32;92m██\e[0m \e[0;1;34;94m▒█\e[0;1;35;95m██\e[0;1;31;91m▒█\e[0;1;33;93m█\e[0m \e[0;1;36;96m░█\e[0;1;34;94m██\e[0;1;35;95m█░\e[0m \e[0;1;32;92m░█\e[0;1;36;96m██\e[0;1;34;94m█░\e[0m \e[0;1;33;93m▒█\e[0;1;32;92m██\e[0;1;36;96m░█\e[0;1;34;94m█\e[0m
+ \e[0;1;35;95m█░\e[0m \e[0;1;33;93m▒█\e[0;1;32;92m█\e[0m
+ \e[0;1;31;91m██\e[0;1;33;93m██\e[0;1;32;92m██\e[0;1;36;96m▓\e[0m
+ \e[0;1;33;93m▒█\e[0;1;32;92m██\e[0;1;36;96m█▒\e[0m
+
+ \e[0;1;36;96m▌\e[0m \e[0;1;31;91m▐\e[0m \e[0;1;33;93m▜\e[0m \e[0;1;36;96m▌\e[0m \e[0;1;34;94m▗\e[0m \e[0;1;35;95m▌\e[0m \e[0;1;31;91m▜\e[0m
+ \e[0;1;32;92m▌\e[0m \e[0;1;34;94m▌▛\e[0;1;35;95m▀▖\e[0;1;31;91m▝▀\e[0;1;33;93m▖▜\e[0;1;32;92m▀\e[0m \e[0;1;36;96m▞\e[0;1;34;94m▀▖\e[0;1;35;95m▞▀\e[0;1;31;91m▖▌\e[0m \e[0;1;33;93m▌\e[0;1;32;92m▐\e[0m \e[0;1;36;96m▞▀\e[0;1;34;94m▌\e[0m \e[0;1;35;95m▛▀\e[0;1;31;91m▖▞\e[0;1;33;93m▀▖\e[0;1;32;92m▞▀\e[0;1;36;96m▘▞\e[0;1;34;94m▀▘\e[0;1;35;95m▄\e[0m \e[0;1;31;91m▛▀\e[0;1;33;93m▖▐\e[0m \e[0;1;32;92m▌\e[0m \e[0;1;36;96m▌\e[0m \e[0;1;34;94m▞\e[0;1;35;95m▀▌\e[0;1;31;91m▞▀\e[0;1;33;93m▖\e[0m \e[0;1;32;92m▌\e[0m \e[0;1;36;96m▌\e[0;1;34;94m▙▀\e[0;1;35;95m▖▞\e[0;1;31;91m▀▖\e[0;1;33;93m▛▀\e[0;1;32;92m▖▞\e[0;1;36;96m▀▌\e[0m
+ \e[0;1;36;96m▐\e[0;1;34;94m▐▐\e[0m \e[0;1;35;95m▌\e[0m \e[0;1;31;91m▌\e[0;1;33;93m▞▀\e[0;1;32;92m▌▐\e[0m \e[0;1;36;96m▖\e[0m \e[0;1;34;94m▌\e[0m \e[0;1;35;95m▖\e[0;1;31;91m▌\e[0m \e[0;1;33;93m▌▌\e[0m \e[0;1;32;92m▌\e[0;1;36;96m▐\e[0m \e[0;1;34;94m▌\e[0m \e[0;1;35;95m▌\e[0m \e[0;1;31;91m▙▄\e[0;1;33;93m▘▌\e[0m \e[0;1;32;92m▌\e[0;1;36;96m▝▀\e[0;1;34;94m▖▝\e[0;1;35;95m▀▖\e[0;1;31;91m▐\e[0m \e[0;1;33;93m▌\e[0m \e[0;1;32;92m▌▐\e[0m \e[0;1;36;96m▚\e[0;1;34;94m▄▌\e[0m \e[0;1;35;95m▚\e[0;1;31;91m▄▌\e[0;1;33;93m▌\e[0m \e[0;1;32;92m▌\e[0m \e[0;1;36;96m▐▐\e[0;1;34;94m▐\e[0m \e[0;1;35;95m▌\e[0m \e[0;1;31;91m▌\e[0m \e[0;1;33;93m▌\e[0;1;32;92m▌\e[0m \e[0;1;36;96m▌▚\e[0;1;34;94m▄▌\e[0m
+ \e[0;1;35;95m▘▘\e[0m \e[0;1;31;91m▘\e[0m \e[0;1;33;93m▘\e[0;1;32;92m▝▀\e[0;1;36;96m▘\e[0m \e[0;1;34;94m▀\e[0m \e[0;1;35;95m▝\e[0;1;31;91m▀\e[0m \e[0;1;33;93m▝▀\e[0m \e[0;1;32;92m▝\e[0;1;36;96m▀▘\e[0m \e[0;1;34;94m▘\e[0;1;35;95m▝▀\e[0;1;31;91m▘\e[0m \e[0;1;33;93m▌\e[0m \e[0;1;32;92m▝\e[0;1;36;96m▀\e[0m \e[0;1;34;94m▀▀\e[0m \e[0;1;35;95m▀\e[0;1;31;91m▀\e[0m \e[0;1;33;93m▀▘\e[0;1;32;92m▀▀\e[0m \e[0;1;34;94m▘▗\e[0;1;35;95m▄▘\e[0m \e[0;1;31;91m▗\e[0;1;33;93m▄▘\e[0;1;32;92m▝▀\e[0m \e[0;1;34;94m▘\e[0;1;35;95m▘\e[0m \e[0;1;31;91m▘\e[0m \e[0;1;33;93m▝\e[0;1;32;92m▀\e[0m \e[0;1;36;96m▘\e[0m \e[0;1;34;94m▘▗\e[0;1;35;95m▄▘\e[0m
--- /dev/null
+snapd (2.21) xenial; urgency=medium
+
+ * New upstream release, LP: #1656382
+ - daemon: re-enable reexec
+ - interfaces: allow reading installed files from previous revisions
+ by default
+ - daemon: make activation optional
+ - tests: run all snap-confine tests in c-unit-tests task
+ - many: fix abbreviated forms of disconnect
+ - tests: switch more tests to MATCH
+ - store: export userAgent. daemon: print store.UserAgent() on
+ startup.
+ - tests: test classic confinement `snap list` and `snap info`
+ output
+ - debian: skip snap-confine unit tests on nocheck
+ - overlord/snapstate: share code between Update and UpdateMany, so
+ that it deals with auto-aliases correctly
+ - interfaces: upower-observe: refactor to allow snaps to provide a
+ slot
+ - tests: add end-to-end store test for classic confinement
+ - overlord,overlord/snapstate: have UpdateMany retire/enable auto-
+ aliases even without new revision
+ - interfaces/browser-support: add @{PROC}/@{pid}/fd/[0-9] w and misc
+ /run/udev
+ - interfaces/builtin: add physical-memory-* and io-ports-control
+ - interfaces: allow getsockopt by default since it is so commonly
+ used
+ - cmd/snap, daemon, overlord/snapstate: tests and fixes for "snap
+ refresh" of a classic snap
+ - interfaces: allow read/write access of real-time clock with time-
+ control interface
+ - store: request no CDN via a header using SNAPPY_STORE_NO_CDN
+ envvar
+ - snap: add information about tracking channel (not just actual
+ channel)
+ - interfaces: use fewer dot imports
+ - overlord/snapstate: remove restrictions on ResetAliases
+ - overlord, store: move confinement filtering to the overlord (from
+ The Store)
+ - many: move interface test helpers to ifacetest package
+ - many: implement 'snap aliases'
+ - vet: fix for unkeyed fields error on aliases_test.go
+ - interfaces: miscellaneous policy updates for network-control,
+ unity7, pulseaudio, default and home
+ - tests: test for auto-aliases
+ - interface hooks: connect plug slot hooks (step 2)
+ - cmd/snap: fix internal naming in snap connect
+ - snap: use "size" as the json tag in snap.ChannelSnapInfo
+ - tests: restore the missing initialization of iface manager causing
+ race
+ - snap: fix missing sizes in `snap info <remote-snap>`
+ - tests: improve cleanup for c-unit-tests
+ - cmd/snap-confine: build non-installed libsnap-confine-private.a
+ - cmd/snap-confine: small tweaks to seccomp support code
+ - interfaces/docker-support: allow /run/shm/aufs.xeno for 14.04
+ - many: obtain installed snaps developer/publisher username through
+ assertions
+ - store: setting of fields for details endpoint
+ - cmd/snap-confine: check for rst2man on configure
+ - snap: show `snap --help` output when just running `snap`
+ - interface/builtin: drop the obsolete checks in udisks2
+ SanitizeSlot
+ - cmd/snap: remove currency switch following UX review
+ - spread: find top-level directory before running generate-
+ packaging-dir
+ - interface hooks: prepare plug slot hooks (step 1)
+ - i18n: use github.com/mvo5/gettext.go (pure go) for i18n to avoid
+ cgo
+ - many: put a marker in the User-Agent sent by snapd/snap when under
+ testingThe User-Agent will look like:
+ - tests: fix -reuse and -resend when govendor is missing
+ - snap: provide friendlier `snap find` message when no snaps are
+ found
+ - tests: fix mkversions.sh failure on zesty
+ - spread: install build-essentail unconditionally
+ - spread: improve qemu ubuntu-14.04-{32,64} support
+ - overlord/snapstate,daemon: implement GET /v2/aliases handling
+ - store: retry user info request
+ - tests: port more snap-confine regression tests
+ - tests: cancel the scheduled reboot on ubuntu-core-upgrade-no-gc
+ and restore state
+ - tests: debug zesty autopkgtest failures
+ - overlord/snapstate: use keyed fields on literals
+ - tests: use MATCH in install-remove-multi
+ - tests: increase wait time for service to be up
+ - tests: make debug-each succeed if DENIED doesn't match
+ - tests: skip packaging dir generation for non-git based autopkgtest
+ runs
+ - tests: port refresh-all-undo to MATCH
+ - tests: improve snap connect test
+ - tests: port additional snap-confine regression tests
+ - tests: show --version when it matches unknown
+ - tests: optionally use apt proxy for qemu
+ - tests: add hello-classic test
+ - many: behave more consistently when pointed to staging and
+ possibly the fake store
+ - overlord/ifacestate: remove stale comments
+ - interfaces/apparmor: ignore snippets in classic confinement
+ - tests: port first regression test from snap-confine
+ - cmd/snap-confine: disable old tests
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 13 Jan 2017 19:39:51 +0100
+
+snapd (2.20.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1648520
+ - tests: enable the ppc64el tests again
+ - tests: add classic confinement test
+ - tests: run snap confine tests in debian/rules already
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 19 Dec 2016 11:53:29 +0100
+
+snapd (2.20) xenial; urgency=medium
+
+ * New upstream release, LP: #1648520
+ - many: implement "snap alias --reset" using snapstate.ResetAliases
+ - debian: use a packaging branch for 14.04
+ - store: retry downloads on io.Copy errors and sha3 checksum errors
+ - snap: show apps in `snap info`
+ - store: send an explicit X-Ubuntu-Classic header to the store
+ - overlord/snapstate: implement snapstate.ResetAliases
+ - interfaces/builtin: add dbus interface
+ - tests: fix tests on 17.04
+ - store: use mocked retry strategy to make store tests faster
+ - overlord: apply auto-aliases information from the snap-declaration
+ on install or refresh
+ - many: prepare landing on trusty
+ - many: implement snap unalias using snapstate.Unalias
+ - overlord/snapstate: fixing the placement/grouping of some
+ functions
+ - interfaces: support network namespaces via 'ip netns' in network-
+ control
+ - interfaces/builtin: fix pulseaudio apparmor rules
+ - interfaces/builtin: add iio interface
+ - tests: update custom core snap with the freshly build snap-confine
+ - interfaces: use sysd.{Disable,Stop} instead of sysd.DisableNow()
+ - overlord,overlord/snapstate: implement snapstate.Unalias by
+ generalizing the "alias" task
+ - interfaces: misc openstack snap enablement
+ - cmd/snap: mock terminal.ReadPassword instead of using /dev/ptmx
+ - notifications, daemon: kill the unsupported events endpoint
+ - client: only allow Dangerous option in InstallPath
+ - overlord/ifacestate: no interface checks if no snap id
+ - many: implement alias command
+ - snap: tweak snap install output as designed by Mark
+ - debian: fix Pre-Depends on dpkg
+ - tests: check if snap-confine --version is unknown
+ - cmd/snap-confine: allow content interface mounts
+ - tests: remove ppa:snappy-dev/image again
+ - interfaces/apparmor: allow access to core snap
+ - tests: remove snap-confine/ubuntu-core-launcher after the tests
+ - overlord,overlord/snapstate: implement snapstate.Alias
+ - cmd/snap: reject "snap disconnect foo"
+ - debian: add split ubuntu-core-launcher and snap-confine packages
+ - cmd: fix mkversion.sh and add regression test
+ - overlord/snapstate: setup/remove aliases as we link/unlink snaps
+ - cmd/snap,tests: alias support in snap run
+ - snap/snapenv: don't obscure HOME if snap uses classic confinement
+ - store: decode response.Body json inside retry loops
+ - cmd/snap-confine: fix compilation on platforms with gcc < 4.9.0
+ - vendor: update tomb package fixing context support
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 15 Dec 2016 22:07:08 +0100
+
+snapd (2.19) xenial; urgency=medium
+
+ * New upstream release, LP: #1648520
+ - cmd/snap-confine: disable support for XDG_RUNTIME_DIR
+ - cmd/snap-confine/tests: fix stale path after move to snapd
+ - cmd/snap-confine: don't use __attribute__((nonull))
+ - snap: add description to `snap info`
+ - snap: show last refresh time
+ - store: switch default delta format from xdelta to xdelta3
+ - interfaces: fix system-observe interface to work with ps_mem
+ - debian: add missing ca-certificates dependency
+ - cmd/snap-confine: add support for classic confinement
+ - snapstate/backend: add backend methods to manage aliases
+ - tests: re-enable snap-confine unit tests via spread
+ - many: merge snap-confine into snapd
+ - many: add support for classic confinement
+ - snap: abort install with ctrl+c
+ - cmd/snap: change terms accept URL following UX review
+ - interfaces/apparmor: use distinct apparmor template for classic
+ - snap: add snap size to `snap info`
+ - interfaces: add unconfined access to modem-manager
+ - snap: support for parsing and exposing on snap.Info aliases
+ - debian: disable autopkgtests on ppc64el
+ - snap: disable support for socket activation
+ - tests: fix incorrect restore of the current symlink
+ - asserts: introduce auto-aliases header in snap-declaration
+ - interfaces/seccomp: add support for classic confinement
+ - tests: do not use external snaps
+ - daemon: close the dup()ed file descriptor to not leak it
+ - overlord, daemon, progress: enable building snapd without CGO
+ - daemon, store: let snap info find things in any channel
+ - store: retry tweaks and logging
+ - snap: Improve `snap --help` output as designed by Mark
+ - interfaces/builtin: fix incorrect udev rule in i2c
+ - overlord: increase test timeout and improve failure message
+ - snap: remove unused experimental command
+ - debian: remove unneeded conflict against the "snappy" package
+ - daemon, strutil: move daemon.quotedNames to strutil.Quoted
+ - docs: document SNAP_DEBUG_HTTP in HACKING.md
+ - cmd/snap: have some completers
+ - snap: support "daemon: notify" in snap.yaml
+ - snap: fix try command when daemon linie is added
+ - interfaces: apparmor support for classic confinement
+ - debian/rules: build with -buildoptions=pie
+ - tests: include /boot in saved state (including bootenv and any
+ kernels)
+ - daemon: ensure `snap try` installs core if it's missing
+ - tests: save/restore /snap/core/current symlink
+ - tests: decrease the number of expected featured apps
+ - tests: add set -e to the prepare ssh script
+ - cmd/snap: add tests for section completion; fix bugs.
+ - cmd/snap: document 'snap list --all'
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 08 Dec 2016 16:16:04 +0100
+
+snapd (2.18.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1644625
+ - daemon: fix crash when `snap refresh` contains a single update
+ - fix unhandled error from io.Copy() in download()
+ - interfaces/builtin: fix incorrect udev rule in i2c
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 05 Dec 2016 15:04:13 +0100
+
+snapd (2.18) xenial; urgency=medium
+
+ * New upstream release, LP: #1644625
+ - store: retry on io.EOF
+ - tests: skip pty tests on ppc64el and powerpc
+ - client, cmd/snap: introducing "snap info"
+ - snap: do exit 0 on install/remove if that snap is already
+ installed or already removed
+ - snap: add `snap watch <change-id>` to attach to a running change
+ - store: retry downloads using retry loop
+ - snap: try doesn't require snap-dir when run in snap's directory
+ - daemon: show what will change in the "refresh-all" changes
+ - tests: disable autorefresh for the external backend
+ - snap: add `snap list -a` to show all snaps (even inactive ones)
+ - many: unify boolean env var handling
+ - overlord/ifacestate: don't setup jailmode snaps with devmode
+ confinement
+ - snapstate: do not garbage collect the snaps used by the bootenv
+ - debian: drop hard xdelta dependency for now
+ - snap: make `snap login` ask for email if not given as argument
+ - osutil: fix build on armhf (arm in go-arch) and powerpc (ppc in
+ go-arch)
+ - many: rename DevmodeConfinement to DevModeConfinement
+ - store: resp.Body.Close() missing in ReadyToBuy
+ - many: use ConfinementOptions instead of ConfinementType
+ - snap, daemon, store: fake the channel map in the REST API
+ - misc: run github.com/gordonklaus/ineffassign as part of the static
+ checks
+ - docs: add goreportcard badge and remove coveralls badge
+ - tests: force gofmt -s in static checks
+ - many: run gofmt -s -w on all the code
+ - store: DRY actual retry code
+ - many: fix various errors uncovered by goreportcard.com
+ - interfaces/builtin: allow additional shared memory for webkit
+ - many: some more missing snapState->snapst
+ - asserts: introduce an optional freeform display-name for model
+ - interfaces/builtin: rename usb-raw to raw-usb
+ - progress: init pbar with correct total value
+ - daemon/api.go: add quotedNames() helper
+ - interfaces: add ConfinementOptions type
+ - tests: add a test about the extra bits that prepare-device can
+ specify for device registration
+ - tests: check that gpio device nodes are exported after reboot
+ - tests: parameterize core channel with env var for classic too
+ - many: rename variable "ss" to "snapsup" or "snapst" or "st"
+ (depending on context)
+ - tests: do not use external snaps in spread
+ - store: retry buy request
+ - store: retry store.Find
+ - store: retry assertion store call
+ - store: retry call for snap details
+ - many: use snap.ConfinementType rather than bool devmode
+ - daemon: if a bad snap is posted it is not an internal error but a
+ bad request
+ - client: add "Snap.Screenshots" to the client API
+ - interfaces: update base declaration documentation and policy for
+ on-classic and snap-type
+ - store: check payment method before TOS for a better UX
+ - interfaces: allow sched_setaffinity in process-control
+ - tests: parameterize core channel with env var
+ - tests: ensure that the XDG_ env contains at least XDG_RUNTIME_DIR
+ - interfaces: fcitx also listens on the session bus for Qt apps
+ - store: retry ListRefresh
+ - snap: use "Password of <email>:" in the `snap login`
+ - many: reshuffle how we load/inject tests keys so image doesn't
+ need assertstate anymore
+ - store: use range requests if we have a local file already
+ - dirs,interfaces,overlord,snap,snapenv,test: export per-snap
+ XDG_RUNTIME_DIR per user
+ - osutil: make RealUser only look at SUDO_USER when uid==0
+ - tests: do not use the ppa:snappy-dev/image in the tests
+ - store: retry readyToBuy request
+ - tests: increase `expect` timeouts
+ - static tests: add spell check
+ - tests: add debug to all flaky expect tests
+ - systemd: correct the mount arguments when mounting with squashfuse
+ - interfaces: add avahi-observe
+ - store: bring delta downloads back
+ - interfaces: add alsa
+ - interfaces/builtin: fix a broken test that snuck into master
+ - osutil: add chattr funcs
+ - image: init "snap_mode" on image creation time to avoid ugly
+ messages
+ - tests: test-snapd-fuse-consumer needs python-fuse as a build-
+ package
+ - interfaces/builtin: add i2c interface
+ - interfaces: add ofono interface
+ - tests: do not use hello-world in our tests
+ - snap: add support for classic confinement
+ - interfaces: remove LegacyAutoConnect() from the interfaces
+ - interfaces: miscellaneous policy updates
+ - tests: run autopkgtests in the autopkgtest.ubuntu.com
+ infrastructure
+ - Implement lxd-client interface exposing the lxd snap
+ - asserts: validate optional account username
+ - many: remove unnecessary snap name parameter from buying endpoint
+ - tests: do not hardcode the size of /dev/ram0
+ - tests: add test that ensures the right content for /etc/os-release
+ - spread tests: fix snap mode check
+ - docs: fix path for source files location in HACKING.md
+ - interfaces/builtin/mir: allow slot to make recvfrom syscalls
+ - store: sections/featured snaps store support
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 24 Nov 2016 19:43:08 +0100
+
+snapd (2.17.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1637215:
+ - release: os-release on core has changed
+ - tests: /dev/ptmx does not work on powerpc, skip here
+ - docs: moved to github.com/snapcore/snapd/wiki (#2258)
+ - debian: golang is not installable on powerpc, use golang-any
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 04 Nov 2016 18:13:10 +0200
+
+snapd (2.17) xenial; urgency=medium
+
+ * New upstream release, LP: #1637215:
+ - overlord/ifacestate: add unit tests for undo of setup-snap-
+ security (#2243)
+ - daemon,overlord,snap,tests: download to .partial in final dir
+ (#2237)
+ - overlord/state: marshaling tests for lanes (#2245)
+ - overlord/state: introduce state lanes (#2241)
+ - overlord/snapstate: fix revert+refresh (#2224)
+ - interfaces/sytemd: enable/disable generated service units (#2229)
+ - many: fix incorrect security files generation on undo
+ - overlord/snapstate: add dynamic snapdX.Y assumes (#2227)
+ - interfaces: network-manager: give slot full read-write access to
+ /run/NetworkManager
+ - docs: update the name of the command for the cross-build
+ - overlord/snapstate: fix missing argument to Noticef
+ - snapstate: ensure gadget/core/kernel can not be disabled (#2218)
+ - asserts: limit to 1y only if len(models) == 0 (#2219)
+ - debian: only install share/locale if available (missing on
+ powerpc)
+ - overlrod/snapstate: fix revert followed by refresh to old-current
+ (#2214)
+ - interfaces/builtin: network-manager and bluez can change hostname
+ (#2204)
+ - snap: switch the auto-import dir to /run/snapd/auto-import
+ - docs: less details about cloud.cfg as requested in trello (#2206)
+ - spread.yaml: Ensure ubuntu user has passwordless sudo for
+ autopkgtests (#2201)
+ - interfaces/builtin: add dcdbas-control interface
+ - boot: do not set boot to try mode if the revision is unchanged
+ - interfaces: add shutdown interface (#2162)
+ - interfaces: add system-power-control interface
+ - many: use the new systemd backend for configuring GPIOs
+ - overlord/ifacestate: setup security for slots before plugs
+ - snap: spool assertion candidates if snapd is not up yet
+ - store,daemon,overlord: download things to a partials dir
+ - asserts,daemon: implement system-user-authority header/concept
+ - interfaces/builtin: home base declaration rule using on-classic
+ for its policy
+ - interfaces/builtin: finish decl based checks
+ - asserts: bump snap-declaration to allow signing with new-style
+ plugs and slots
+ - overlord: checks for kernel installation/refresh based on model
+ assertion and previous kernel
+ - tests/lib/fakestore: fix logic to distinguish assertion not found
+ errors
+ - client: add a few explicit error types (around the request cycle)
+ - tests/lib/fakestore/cmd/fakestore: make it log, and fix a typo
+ - overlord/snapstate: two bugs for one
+ - snappy: disable auto-import of assertions on classic (#2122)
+ - overlord/snapstate: move trash cleanup to a cleanup handler
+ (#2173)
+ - daemon: make create-user --known fail on classic without --force-
+ managed (#2123)
+ - asserts,interfaces/policy: implement on-classic plug/slot
+ constraints
+ - overlord: check that the first installed gadget matches the model
+ assertion
+ - tests: use the snapd-control-consumer snap from the store
+ - cmd/snap: make snap run not talk to snapd for finding the revision
+ - snap/squashfs: try to hard link instead of copying. Also, switch
+ to osutil.CopyFile for cp invocation.
+ - store: send supported max-format when retrieving assertions
+ - snapstate, devicestate: do not remove seed
+ - boot,image,overlord,partition: read/write boot variables in single
+ operation
+ - tests: reenable ubuntu-core tests on qemu
+ - asserts,interfaces/policy: allow OR-ing of subrule constraints in
+ plug/slot rules
+ - many: move from flags as ints to flags as structs-of-bools (#2156)
+ - many: add supports for keeping and finding assertions with
+ different format iterations
+ - snap: stop using ubuntu-core-launcher, use snap-confine
+ - many: introduce an assertion format iteration concept, refuse to
+ add unsupported assertion
+ - interfaces: tweak wording and comment
+ - spread.yaml: dump apparmor denials on spread failure
+ - tests: unflake ubuntu-core-reboot (#2150)
+ - cmd/snap: tweak unknown command error message (#2139)
+ - client,daemon,cmd: add payment-declined error kind (#2107)
+ - cmd/snap: update remove command help (#2145)
+ - many: removed frameworks target and fixed service files (#2138)
+ - asserts,snap: validate attributes to a JSON-compatible type subset
+ (#2140)
+ - asserts: remove unused serial-proof type
+ - tests: skip auto-import tests on systems without test keys (#2142)
+ - overlord/devicestate: don't spam the debug log on classic (#2141)
+ - cmd/snap: simplify auto-import mountinfo parsing (#2135)
+ - tests: run ubuntu-core upgrades on isolated machine (#2137)
+ - overlord/devicestate: recover seeding from old external approach
+ (#2134)
+ - overlord: merge overlord/boot pkg into overlord/devicestate
+ (#2118)
+ - daemon: add postCreateUserSuite test suite (#2124)
+ - tests: abort tests if an update process is scheduled (#2119)
+ - snapstate: avoid reboots if nothing in the boot setup has changed
+ (#2117)
+ - cmd/snap: do not auto-import from loop or non-dev devices (#2121)
+ - tests: add spread test for `snap auto-import` (#2126)
+ - tests: add test for auto-mount assertion import (#2127)
+ - osutil: add missing unit tests for IsMounted (#2133)
+ - tests: check for failure creating user on managed ubuntu-core
+ systems (#2096)
+ - snap: ignore /dev/loop addings from udev (#2111)
+ - tests: remove snapd.boot-ok reference (#2109)
+ - tests: enable tests related to the home interface in all-snaps
+ (#2106)
+ - snapstate: only import defaults from gadget on install (#2105)
+ - many: move firstboot code into the snapd daemon (#2033)
+ - store: send correct JSON type of string for expected payment
+ amount (#2103)
+ - cmd/snap: rename is-managed to managed and tune (#2102)
+ - interfaces,overlord/ifacestate: initial cleaning up of no arg
+ AutoConnect related bits (#2090)
+ - client, cmd: prompt for password when buying (#2086)
+ - snapstate: fix hanging `snap remove` if snap is no longer mounted
+ - image: support gadget specific cloud.conf file (#2101)
+ - cmd/snap,ctlcmd: fix behavior of snap(ctl) get (#2093)
+ - store: local users download from the anonymous url (#2100)
+ - docs/hooks.md: fix typos (#2099)
+ - many: check installation of slots and plugs against declarations
+ - docs: fix missing "=" in the systemd-active docs
+ - store: do not set store auth for local users (#2092)
+ - interfaces,overlord/ifacestate: use declaration-based checking for
+ auto-connect (#2071)
+ - overlord, daemon, snap: support gadget config defaults (#2082)The
+ main semantic changes are:
+ - tests: fix snap-disconnect tests after core rename (#2088)
+ - client,daemon,overlord,cmd: add /v2/users and create-user on auto-
+ import (#2074)
+ - many: abbreviated forms of disconnect (#2066)
+ - asserts: require lowercase model until insensitive matching is
+ ready (#2076)
+ - cmd/snap: add version command, same as --version (#2075)
+ - all: use "core" by default but allow "ubuntu-core" still (#2070)
+ - overlord/devicestate, docs/hooks.md: nest prepare-device
+ configuration options
+ - daemon: fix login API to return local macaroons (#2078)
+ - daemon: do not hardcode UID in userLookup (#2080)
+ - client, cmd: connect fixes (#2026)
+ - many: preparations for switching most of autoconnect to use the
+ declarationsfor now:
+ - overlord/auth: update CheckMacaroon to verify local snapd
+ macaroons (#2069)
+ - cmd/snap: trivial auto-import and download tweaks (#2067)
+ - interfaces: add repo.ResolveConnect that handles name resolution
+ - interfaces/policy: introduce InstallCandidate and its checks
+ - interfaces/policy,overlord: check connection requests against the
+ declarations in ifacestate
+ - many: setup snapd macaroon for local users (#2051)Next step: do
+ snapd macaroons verification.
+ - interfaces/policy: implement snap-id/publisher-id checks
+ - many: change Connect to take ConnRef instead of strings (#2060)
+ - snap: auto mount block devices and import assertions (#2047)
+ - daemon: add `snap create-user --force-managed` support (#2041)
+ - docs: remove references to removed buying features (#2057)
+ - interfaces,docs: allow sharing SNAP{,_DATA,_COMMON} via content
+ iface (#2063)
+ - interfaces: add Plug/Slot/Connection reference helpers (#2056)
+ - client,daemon,cmd/snap: improve create-user APIs (#2054)
+ - many: introduce snap refresh --ignore-validation <snap> to
+ override refresh validation (#2052)
+ - daemon: add support for `snap create-user --known` (#2040)
+ - interfaces/policy: start of interface policy checking code based
+ on declarations (#2050)
+ - overlord/configstate: support nested configuration (#2039)
+ - asserts,interfaces/builtin,overlord/assertstate: introduce base-
+ declaration (#2037)
+ - interfaces: builtin: Allow writing DHCP lease files to
+ /run/NetworkManager/dhcp (#2049)
+ - many: remove all traces of the /v2/buy/methods endpoint (#2045)
+ - tests: add external spread backend (#1918)
+ - asserts: parse the slot rules in snap-declarations (#2035)
+ - interfaces: allow read of /etc/ld.so.preload by default for armhf
+ on series 16 (#2048)
+ - store: change purchase to order and store clean up first pass
+ (#2043)
+ - daemon, store: switch to new store APIs in snapd (#2036)
+ - many: add email to UserState (#2038)
+ - asserts: support parsing the plugs stanza i.e. plug rules in snap-
+ declarations (#2027)
+ - store: apply deltas if explicitly enabled (#2031)
+ - tests: fix create-key/snap-sign test isolation (#2032)
+ - snap/implicit: don't restrict the camera iface to classic (#2025)
+ - client, cmd: change buy command to match UX document (#2011)
+ - coreconfig: nuke it. Also, ignore po/snappy.pot. (#2030)
+ - store: download deltas if explicitly enabled (#2017)
+ - many: allow use of the system user assertion with create-user
+ (#1990)
+ - asserts,overlord,snap: add prepare-device hook for device
+ registration (#2005)
+ - debian: adjust packaging for trusty/deputy systemd (#2003)
+ - asserts: introduce AttributeConstraints (#2015)
+ - interface/builtin: access system bus on screen-inhibit-control
+ - tests: add firewall-control interface test (#2009)
+ - snapstate: pass errors from ListRefresh in updateInfo (#2018)
+ - README: add links to IRC, mailing list and social media (#2022)
+ - docs: add `configure` hook to hooks list (#2024)LP: #1596629
+ - cmd/snap,configstate: rename apply-config variables to configure.
+ (#2023)
+ - store: retry download on 500 (#2019)
+ - interfaces/builtin: support time and date settings via
+ 'org.freedesktop.timedate1 (#1832)
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 02 Nov 2016 01:17:36 +0200
+
+snapd (2.16) xenial; urgency=medium
+
+ * New upstream release, LP: #1628425
+ - overlord/state: prune old empty changes
+ - interfaces: ppp: load needed kernel module (#2007)
+ - interfaces/builtin: add missing rule to allow run-parts to
+ execute all resolvconf scripts
+ - many: rename apply-config hook to configure
+ - tests: use new spread `debug` feature
+ - many: finish `snap set` API.
+ - overlord: fix and simplify configstate.Transaction
+ - assertions: add system-user assertion
+ - snap: add `snap known --remote`
+ - tests: replace systemd-run with on-the-fly generation of units.
+ - overlord/boot: switch to using assertstate.Batch
+ - snap, daemon, store: pass through screenshots from store
+ - image: add meta/gadget.yaml infrastructure
+ - tests: add test benchmark script
+ - daemon: add the actual ssh keys that got added to the create-user
+ response
+ - daemon: add REST API behind `snap get`
+ - debian: re-add golang-github-gosexy-gettext-dev
+ - tests: added install_local function
+ - interfaces/builtin: fix resolvconf permissions for network-manager
+ interface
+ - tests: use apt as compatible with trusty
+ - many: discard preserved namespace after removing snap
+ - daemon, overlord, store: add ReadyToBuy API to snapd
+ - many: add support for installing/removing multiple snaps
+ - progress: use New64 and fix output newline
+ - interfaces/builtin: allow network-manager to access netplan conf
+ files
+ - tests: build once and install test snap from cache
+ - overlord/state: introduce cleanup support
+ - snap: move/clarify Info.Broken
+ - ctlcmd: add snapctl get.
+ - overlord,store: clean up serial-proof plumbing code
+ - interfaces/builtin: add network-setup-observe interface
+ - daemon,overlord/assertstate: support streams of assertions with
+ snap ack
+ - snapd: kmod backend
+ - tests: ensure HOME is also set correctly
+ - configstate,hookstate: add snapctl set
+ - tests: disable broken create-key test
+ - interfaces: adjust bluetooth-control to allow getsockopt (LP:
+ #1613572)
+ - tests: add a test for core about device initialization and device
+ registration and auth
+ - many: show snap name before the download progress bar
+ - interfaces/builtin: add rcvfrom for client connected plugs to mir
+ interface
+ - asserts: support for maps in assertions
+ - tests: increase timeout for key generation in create-key test
+ - many: validate refreshes against validation assertions by gating
+ snaps
+ - interfaces/apparmor: allow 'm' in default policy for snap-exec
+ - many: avoid snap.InfoFromSnapYaml in tests
+ - interfaces/builtin: allow /dev/net/tun with network-control
+ - tests: add spread test for snap create-key/snap sign
+ - tests: add missing quotes in security-device-cgroups/task.yaml
+ - interfaces: drop ErrUnknownSecurity
+ - store: add "ready to buy" method
+ - snap/snapenv, tests: use root's data dirs when running via sudo
+ - interfaces/builtin: add initial docker interface
+ - snap: remove extra newline after progress is done
+ - docs: fix formating of HACKING.md "Testing snapd"
+ - store : add requestOptions.ExtraHeaders so that individual
+ requests can customise headers.
+ - many: use unique plug/slot names in tests
+ - tests: add tests for the classic dimension
+ - many: add vendoring of dependencies by default
+ - tests: use in-tree snap{ctl,-exec} for all tests
+ - many: support snapctl -h
+ - tests: adjust regex after changes in stat output
+ - store,snap: initial support for delta downloads
+ - interfaces/builtin: add run/udev/data paths to mir interface
+ - snap: lessen annoyance of implicit interface tests
+ - tests: ensure http{,s}_proxy is defined inside the fake-store
+ - interfaces: allow xdg-open in unity7, unity7 cleanups
+ - daemon,store: move store login user logic to store
+ - tests: replace realpath with readlink -f for trusty support.
+ - tests: add https_proxy into environment as well
+ - interfaces/builtin: allow mmaping pulseaudio buffers
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 28 Sep 2016 11:09:27 +0200
+
+snapd (2.15.2ubuntu1) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - snap/snapenv, tests: use root's data dirs when running via sudo
+ (cherry pick PR: #1857)
+ - tests: add https_proxy into environment
+ (cherry pick PR: #1926)
+ - interfaces: allow xdg-open in unity7, unity7 cleanups
+ (cherry pick PR: #1946)
+ - tests: ensure http{,s}_proxy is defined inside the fake-store
+ (cherry pick PR: #1949)
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 21 Sep 2016 17:21:12 +0200
+
+snapd (2.15.2) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - asserts: define a bit less terse Ref.String
+ - interfaces: disable auto-connect in libvirt interface
+ - asserts: check that validation assertions are signed by the
+ publisher of the gating snap
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 19 Sep 2016 10:42:29 +0200
+
+snapd (2.15.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - image: ensure local snaps are put last in seed.yaml
+ - asserts: revert change that made the account-key's name mandatory.
+ - many: refresh all snap decls
+ - interfaces/apparmor: allow reading /etc/environment
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 19 Sep 2016 09:19:44 +0200
+
+snapd (2.15) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - tests: disable prepare-image-grub test in autopkgtest
+ - interfaces: allow special casing for auto-connect until we have
+ assertions
+ - docs: add a little documentation on hooks.
+ - hookstate,daemon: don't mock HookRunner, mock command.
+ - tests: add http_proxy to /etc/environment in the autopkgtest
+ environment
+ - backends: first bits of kernel-module security backend
+ - tests: ensure openssh-server is installed in autopkgtest
+ - tests: make ubuntu-core tests more robust
+ - many: mostly work to support ABA upgrades
+ - cmd/snap: do runtime linting of descriptions
+ - spread.yaml: don't assume LANG is set
+ - snap: fix SNAP* environment merging in `snap run`
+ - CONTRIBUTING.md: remove integration-tests, include spread
+ - store: don't discard error body from request device session call
+ - docs: add create-user documentation
+ - cmd/snap: match UX document for message when buying without login
+ - firstboot: do not overwrite any existing netplan config
+ - tests: add debug output to ubuntu-core-update-rollback-
+ stresstest:
+ - tests/lib/prepare.sh: test that classic does not setting bootvars
+ - snap: run all tests with gpg2
+ - asserts: basic support for validation assertion and refresh-
+ control
+ - interfaces: miscellaneous policy updates for default, browser-
+ support and camera
+ - snap: (re)add --force-dangerous compat option
+ - tests: ensure SUDO_{USER,GID} is unset in the spread tests
+ - many: clean out left over references to integration tests
+ - overlord/auth,store: fix raciness in updating device/user in state
+ through authcontext and other issuesbonus fixes:
+ - tests: fix spread tests on yakkety
+ - store: refactor auth/refresh tests
+ - asserts: use gpg --fixed-list-mode to be compatible with both gpg1
+ and gpg2
+ - cmd/snap: i18n option descriptions
+ - asserts: required account key name header
+ - tests: add yakkety test host
+ - packaging: make sure debhelper-generated snippet is invoked on
+ postrm
+ - snap,store: capture newest digest from the store, make it
+ DownloadInfo only
+ - tests: add upower-observe spread test
+ - Merge github.com:snapcore/snapd
+ - tests: fixes to actually run the spread tests inside autopkgtest
+ - cmd/snap: make "snap find" error nicer.
+ - tests: get the gadget name from snap list
+ - cmd/snap: tweak help of 'snap download'
+ - cmd/snap,image: teach snap download to download also assertions
+ - interfaces/builtin: tweak opengl interface
+ - interfaces: serial-port use udevUsbDeviceSnippet
+ - store: ensure the payment methods method handles auth failure
+ - overlord/snapstate: support revert flags
+ - many: add snap configuration to REST API
+ - tests: use ubuntu-image for the ubuntu-core-16 image creation
+ - cmd/snap: serialise empty keys list as [] rather than null
+ - cmd/snap,client: add snap set and snap get commands
+ - asserts: update trusted account-key asserts with names
+ - overlord/snapstate: misc fixes/tweaks/cleanups
+ - image: have prepare-image set devmode correctly
+ - overlord/boot: have firstboot support assertion files with
+ multiple assertions
+ - daemon: bail from enable and disable if revision given, and from
+ multi-op if unsupported optons given
+ - osutil: call sync after cp if
+ requested.overlord/snapstate/backend: switch to use osutil instead
+ of another buggy call to cp
+ - cmd/snap: generate account-key-request "since" header in UTC
+ - many: use symlinks instead of wrappers
+ - tests: remove silly [Service] entry from snapd.socket.d/local.conf
+ - store: switch device session to use device-session-request
+ assertion
+ - snap: ensure that plug and slot names are unique
+ - cmd/snap: fix test suite (no Exit(0) on tests!)
+ - interfaces: add interface for hidraw devices
+ - tests: use the real model assertion when creating the core test
+ image
+ - interfaces/builtin: add udisks2 and removable-media interfaces
+ - interface: network_manager: enable resolvconf
+ - interfaces/builtin: usb serial-port support via udev
+ - interfaces/udev: support noneSecurityTag keyed snippets
+ - snap: switch to the new agreed regexp for snap names
+ - tests: adjust test setup after ubuntu user removal
+ - many: start services only after the snap is fully ready (link-snap
+ was run)
+ - asserts: don't have Add/Check panic in the face of unsupported no-
+ authority assertions
+ - asserts: initial support to generate/sign snap-build assertions
+ - asserts: support checking account-key-request assertions
+ - overlord: introduce AuthContext.DeviceSessionRequest with support
+ in devicestate
+ - overlord/state: fix for reloaded task/change crashing on Set if
+ checkpointed w. no custom data yet
+ - snapd.refresh.service: require snap.socket and /snap/*/current.
+ - many: spell --force-dangerous as just --dangerous, devmode should
+ imply it
+ - overlord/devicestate: try to fetch/refresh the signing key of
+ serial (also in case is not there yet)
+ - image,overlord/boot,snap: metadata from asserts for image snaps
+ - many: automatically restart all-snap devices after os/kernel
+ updates
+ - interfaces: modem-manager: ignore camera
+ - firstboot: only configure en* and eth* interfaces by default
+ - interfaces: fix interface handling on no-app snaps
+ - snap: set user variables even if HOME is unset (like with systemd
+ services)
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 16 Sep 2016 07:46:22 +0200
+
+snapd (2.14.2~16.04) xenial; urgency=medium
+
+ * New upstream release: LP: #1618095
+ - tests: use the spread tests with the adhoc interface inside
+ autopkgtest
+ - interfaces: add fwupd interface
+ - asserts,cmd/snap: add "name" header to account-key(-request)
+ - client,cmd/snap: display os-release data only on classic
+ - asserts/tool,cmd/snap: introduce hidden "snap sign"
+ - many: when installing snap file derive metadata from assertions
+ unless --force-dangerous
+ - osutil: tweak the createUserTests a bit and extract common code
+ - debian: umount --lazy before rm on snapd.postrm
+ - interfaces: updates to default policy, browser-support, and x11
+ - store: set initial device session
+ - interfaces: add upower-observe interface (LP: #1595813)
+ - tests: use beta u-d-f in test by default
+ - interfaces/builtin: allow writing on /dev/vhci in bluetooth-
+ control
+ - interfaces/builtin: allow /dev/vhci on bluetooth-control
+ - tests: port integration tests to spread
+ - snapstate: use umount --lazy when removing the mount units
+ - spread: enable halt-timeout, tweak image selection
+ - tests: fix firstboot-assertions to actually be runnable on classic
+ again
+ - asserts: introduce device-session-request
+ - interfaces: add screen-inhibit-control interface (LP: #1604880)
+ - firstboot: change location of netplan config
+ - overlord/devicestate: some cleanups and solving a couple todos
+ - daemon,overlord: add subcommand handling to snapctl
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 01 Sep 2016 18:52:05 +0200
+
+snapd (2.14.1) xenial; urgency=medium
+
+ * New upstream release: LP: #1618095
+ - snap-exec: add support for commands with internal args in snap-
+ exec
+ - store: refresh expired device sessions
+ - debian: re-add ubuntu-core-snapd-units as a transitional package
+ - image: snap assertions into image
+ - overlord/assertstate,asserts/snapasserts: give snap assertions
+ helpers a package, introduce ReconstructSideInfo
+ - docs/interfaces: Add empty line after lxd-support title
+ - README: cover the new /run/snapd-snap.socket
+ - daemon: make socket split backward-compatible.
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 30 Aug 2016 16:43:29 +0200
+
+snapd (2.14) xenial; urgency=medium
+
+ * New upstream release: LP: #1618095
+ - cmd: enable SNAP_REEXEC only if it is set to SNAP_REEXEC=1
+ - osutil: fix create-user on classic
+ - firstboot: disable firstboot on classic for now
+ - cmd/snap: add export-key --account= option
+ - many: split public snapd REST API into separate socket.
+ - many: drop ubuntu-core-snapd-units package, use release.OnClassic
+ instead
+ - tests: add content-shareing binary test that excersises snap-
+ confine
+ - snap: use "up to date" instead of "up-to-date"
+ - asserts: add an account-key-request assertion
+ - asserts: fix GPG key generation parameters
+ - tests, integration-tests: implement the cups-control manual test
+ as a spread test
+ - many: clarify/tie down model assertion
+ - cmd/snap: add "snap download" command
+ - integration-tests: remove them in favour of the spread tests
+ - tests: test all snap ubuntu core upgrade
+ - many: support install and remove by revision
+ - overlord/state: prevent change ready => unready
+ - tests: fixes to make the ubuntu-core-16 image usable with
+ -keep/-reuse
+ - asserts: authority-id and brand-id of serial must match
+ - firstboot: generate netplan config rather than ifupdown
+ - store: request device session macaroon from store
+ - tests: add workaround for u-d-f to unblock all-snap image tests
+ - tests: the stable ubuntu-core snap has snap run support now
+ - many: use make StripGlobalRootDir public
+ - asserts: add some stricter checks around format
+ - many: have AuthContext expose device store-id, serial and serial-
+ proof signing to the store
+ - tests: fix "tests/main/ack" to not break if asserts are alreay
+ there
+ - tests/main/ack: fix test/style
+ - snap: add key management commands
+ - firstboot: add firstboot assertions importing
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 29 Aug 2016 17:07:20 +0200
+
+snapd (2.13) xenial; urgency=medium
+
+ * New upstream release: LP: #1616157
+ - many: respect dirs.SnapSnapsDir in tests
+ - tests: update listing test for latest stable image
+ - many: hook in start of code to fetch/check assertions when
+ installing snap from store
+ - boot: add missing udevadm mock to fix FTBFS
+ - interfaces: add lxd-support interface
+ - dirs,snap: handle empty root directory in SetRootDir
+ - dirs,snap: define methods for SNAP_USER_DATA and SNAP_USER_COMMON
+ - tests: spread all-snap test cleanup
+ - tests: add all-snap spread image tests
+ - store,tests: have just one envvar SNAPPY_USE_STAGING_STORE to
+ control talking to staging
+ - overlord/hookstate: use snap run posix parameters.
+ - interfaces/builtin: allow bind in the network interface
+ - asserts,overlord/devicestate: simplify private key/key pairs APIs,
+ they take just key ids
+ - dependencies: update godeps
+ - boot: add support for "devmode: {true,false}" in seed.yaml
+ - many: teach prepare-image to copy the model assertion (and
+ prereqs) into the seed area of the image
+ - tests: start teaching the fakestore about assertions
+ - asserts/sysdb: embed the new format official root/trusted
+ assertions
+ - overlord/devicestate: first pass at device registration logic
+ - tests: add process-control interface spread test
+ - tests: disable unity test
+ - tests: adapt to new spread version
+ - asserts: add serial-proof device assertion
+ - client, cmd/snap: use the new multi-refresh endpoint
+ - many: preparations for image code to fetch model prereqs
+ - debian: add extra checks when debian/snapd.postrm purge is run
+ - overlord/snapstate, daemon: support for multi-snap refresh
+ - tests: do not leave "squashfs-root" around
+ - snap-exec: Fix broken `snap run --shell` and add test
+ - overlord/snapstate: check changes to SnapState for conflicts also.
+ - docs/interfaces: change snappy command to snap
+ - tests: test `snap run --hook` using in-tree snap-exec.
+ - partition: ensure that snap_{kernel,core} is not overridden with an
+ empty value
+ - asserts,overlord/assertstate: introduce an assertstate task
+ handler to fetch snap assertions
+ - spread: disable re-exec to always test development tree.
+ - interfaces: implement a fuse interface
+ - interfaces/hardware-observe.go: re-add /run/udev/data
+ - overlord/assertstate,daemon: reorg how the assert manager exposes
+ the assertion db and adding to it
+ - release: Remove "UBUNTU_CODENAME" from the test data
+ - many: implement snapctl command.
+ - interfaces: mpris updates (fix unconfined introspection, add name
+ attribute)
+ - asserts: export DecodePublicKey
+ - asserts: introduce support for assertions with no authority,
+ implement serial-request
+ - interfaces: bluez: add a few more tests to verify interface
+ connection works
+ - interfaces: bluez: add missing mount security snippet case
+ - interfaces: add kernel-module interface for module insertion.
+ - integration-tests: look for ubuntu-device-flash on PATH before
+ calling sudo
+ - client, cmd, daemon, osutil: support --yaml and --sudoer flags for
+ create-user
+ - spread: use snap-confine from ppa:snappy-dev/image for the tests
+ - many: move to purely hash based key lookup and to new
+ key/signature format (v1)
+ - spread: Use /home/gopath in spread.yaml
+ - tests: base security spread tests
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 24 Aug 2016 14:48:28 +0200
+
+snapd (2.12) xenial; urgency=medium
+
+ * New upstream release: LP: #1612362
+ - many: do not require root for `snap prepare-image`
+ - tests: prevent restore error on test failure
+ - osutil: change escaping for create-user's sudoers
+ - docs: private flag doesn't exist on /v2/find (it's select)
+ - snap: do not sort the result of `snap find`
+ - interfaces/builtin: add gpio interface
+ - partition: fix cleaning of the boot variables on the second good
+ boot
+ - tests: add udev rules spread test
+ - docs: fix references to refresh action
+ - interfaces/udev,osutil: avoid doubled rules and put all in a per
+ snap file
+ - store: minor store improvements from previous reviews
+ - many: support interactive payments in snapd, filter from command
+ line
+ - docs/interfaces.md: improve interfaces documentation
+ - overlord,store: set store device authorization header
+ - store: add device nonce API support
+ - many: various fixes around the `create-user` command
+ - client, osutil: chown the auth file
+ - interfaces/builtin: add transitional browser-support interface
+ - snap: don't load unsupported implicit hooks.
+ - cmd/snap,cmd/snap-exec: support hooks again.
+ - interfaces/builtin: improve pulseaudio interface
+ - asserts: make account-key's `until` optional to represent a never-
+ expiring key
+ - store: refactor newRequest/doRequest to take requestOptions
+ - tests: allow-downgrades on upgrade test to prevent version errors
+ - daemon: stop using group membership as succedaneous of running
+ things with sudo
+ - interfaces: add bluetooth-control interfaces
+ - many: remove integration-test coverage metrics
+ - daemon,docs: drop license docs and error kind
+ - tests: add network-control interface spread test
+ - tests: add hardware-observe spread test
+ - interfaces: add system-trace interface LP: #1600085
+ - boot: use `cp -aLv` instead of `cp -a` (no symlinks on vfat)
+ - store: soft-refresh discharge macaroon from store when required
+ - partition: clear snap_try_{kernel,core} on success
+ - tests: add snapd-control interface spread test
+ - tests: add locale-control write spread test
+ - store: fix buy method after some refactoring broke it
+ - interfaces/builtin: read perms for network devices in network-
+ observe
+ - interfaces: also allow rfkill in network_control
+ - snapstate: remove artifacts from a snap try dir that vanished
+ - client, cmd/snap: better errors for empty snap list result
+ - wrappers: set BAMF_DESKTOP_FILE_HINT for unity
+ - many: cleanup/update rest.md; improve auth errors
+ - interfaces: miscelleneous policy updates for default, log-observe,
+ mount-observe, opengl, pulseaudio, system-observe and unity7
+ - interfaces: add process-control interface (LP: #1598225)
+ - osutil: support both "nobody" and "nogroup" for grpnam tests
+ - cmd: support defaulting to the user's preferred payment method
+ - overlord: actually run hooks.
+ - overlord/state,overlord/ifacestate: define basic infrastructure
+ for and then setting up serialising of interface mgr tasks
+ - asserts: add Assertion.Prerequisites and SigningKey, Ref and
+ FindTrusted
+ - overlord/snapstate: ensure calls to store are done without the
+ state lock held
+ - asserts,client: switch snap-build and snap-revision to be indexed
+ by snap-sha3-384
+ - many: make seed.yaml on firstboot mandatory and include sideInfo
+ - asserts,many: start supporting structured headers using the new
+ parseHeaders
+ - many: update code for the new snap_mode
+ - tests: added spread find private test
+ - store: deal with 404 froms the SSO store properly
+ - snap: remove meta/kernel.yaml again
+ - daemon: always mock release info in tests
+ - snapstate: drop revisions after "current" on refresh
+ - asserts: introduce new parseHeadersThis introduces the new
+ parseHeaders returning map[string]interface{} and capable of
+ accepting:
+ - asserts: remove/disable comma separated lists and their uses
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 11 Aug 2016 19:30:36 +0200
+
+snapd (2.11) xenial; urgency=medium
+
+ * New upstream release: LP: #1605303
+ - increase version number to reflect the nature of the update
+ better
+ - store, daemon, client, cmd/snap, docs/rest.md: adieu search
+ grammar
+ - debian: move snapd.refresh.timer into timers.target
+ - snapstate: add daemon-reload to fix autopkgtest on yakkety
+ - Interfaces: hardware-observe
+ - snap: rework the output after a snap operation
+ - daemon, cmd/snap: refresh --devmode
+ - store, daemon, client, cmd/snap: implement `snap find --private`
+ - tests: add network-observe interface spread test
+ - interfaces/builtin: allow getsockopt for connected x11 plugs
+ - osutil: check for nogrup instead of adm
+ - store: small cleanups (more needed)
+ - snap/squashfs: fix test not to hardcode snap size
+ - client,cmd/snap: cleanup cmd/snap test suite, add extra args
+ testThis cleans up the cmd/snap test suite:
+ - wrappers: map "never" restart condition to "no."
+ - wrappers: run update-desktop-database after add/remove of desktop
+ files
+ - release: work around elementary mistake
+ - many: remove all traces of channel from the buying codepath
+ - store: kill setUbuntuStoreHeaders
+ - docs: add payment methods documentation
+ - many: present user with a choice of payment backends
+ - asserts: add cross checks for snap asserts
+ - cmd/snap,cmd/snap-exec: support running hooks via snap-exec.
+ - tests: improve snap run symlink tests
+ - tests: add content sharing interface spread test
+ - store & many: a mechanical branch shortening store names
+ - snappy: remove old snappy pkg
+ - overlord/snapstate: kill flagscompat
+ - overlord/snapstate, daemon, client, cmd/snap: devmode override
+ (aka confined)
+ - tests: extend refresh test to talk to the staging and production
+ stores
+ - asserts,daemon: cross checks for account and account-key
+ assertions
+ - client: existing JSON fixtures uses tabs for indentation
+ - snap-exec: add proper integration test for snap-exec
+ - spread.yaml, tests: replace hello-world with test-snapd-tools
+ - tests: add locale-control interface spread test
+ - tests: add mount-observe interface spread test
+ - tests: add system-observe interface spread test
+ - many: add AuthContext to mediate user updates to the state
+ - store/auth: add helper for the macaroon refresh endpoint
+ - cmd: add buy command
+ - overlord: switch snapstate.Update to use ListRefresh (aka
+ /snaps/metadata)
+ - snap-exec: fix silly off-by-one error
+ - tests: stop using hello-world.echo in the tests
+ - tests: add env command to test-snapd-tools
+ - classic: remove (most of) "classic" mode, this is implemented as a
+ snap now
+ - many: remove snapstate.Candidate and other cleanups
+ - many: removed authenticator, store gets a user instead
+ - asserts: fix minor doc comment typo
+ - snap: ensure unknown arguments to `snap run` are ignored
+ - overlord/auth: add Device/SetDevice to persist device identity in
+ state
+ - overlord: make SyncBoot work again
+ - tests: add -y flag to apt autoremove command in unity task restore
+ - many: migrate SnapSetup and SideInfo to use RealName
+ - daemon: drop auther()
+ - client: improve error from client.do() on json decode failures
+ - tests: readd the fake store tests
+ - many: allow removal of broken snaps, add spread test
+ - overlord: implement &Retry{After: duration} support for handlers
+ - interface: add new interfaces.all.SecurityBackends
+ - integration-tests: remove login tests
+ - cmd,interfaces,snap: implement hook whitelist.
+ - daemon,overlord/auth,store: update macaroon authentication to use
+ the new endpoints
+ - daemon, overlord: add buy endpoint to REST API
+ - tests: use systemd-run for starting and stopping the unity app
+ - tests, integration-tests: port systemd service check test to
+ spread
+ - store: switch search to new snap-specific endpoint
+ - store, many: start using the new details endpoint
+ - tests, integration-tests: port unity test to spread
+ - tests: add spread test for tried snaps removal
+ - tests, integration-tests: port auth errors test to spread
+ - snapstate: rename OfficialName to RealName in the new tests
+ - many: rename SideInfo.OfficialName to SideInfo.RealName
+ - snapstate: use snapstate.Type in backend.RemoveSnapFiles
+ - many: add `snap enable/disable` commands
+ - tests, integration-tests: port refresh all test to spread
+ - snap: add `snap run --shell`
+ - tests: set yaml indentation to 4 spaces
+ - snapstate: cleanup downloaded temp snap files
+ - overlord: make patch1_test more robust
+ - debian: add snapd.postrm that purges
+ - integration-tests: drop already covered refresh app test
+ - many: add concept of "broken" snaps
+ - tests, integration-tests: port remove errors tests to spread
+ - tests, integration-tests: port revert test to spread
+ - debian: fix snapbuild path
+ - overlord: fix access to the state without lock in firstboot.go and
+ add test
+ - snapstate: add very simple garbage collection on upgrade
+ - asserts: introduce assertstest with helpers to test code involving
+ assertions
+ - tests, integration tests: port undone failed install test to
+ spread
+ - snap,store: switch to the new snaps/metadata endpoint, introduce
+ and start capturing DeveloperID
+ - tests, integration-tests: port the op remove retry test to spread
+ - po: remove snappy.pot from git, it will be generated at build time
+ - many: add some missing tests, clarify some things and nitpicks as
+ follow up to `snap revert`
+ - snapstate: when doing snapsate.Update|Install, talk to the store
+ early
+ - tests, integration-tests: port the op remove test to spread
+ - interfaces: allow /usr/bin/locale in default policy
+ - many: add `snap revert`
+ - overlord/auth,store: add macaroon serialization/deserialization
+ helpers
+ - many: embed main store trusted assertions in snapd, way to have
+ test ones, spread tests for ack and known
+ - overlord/snapstate,daemon: clarify active vs current, add
+ SnapState.HasCurrent,CurrentInfo
+ - tests: do not search for a specific snap (we hit 100 items) and
+ pagination kicks in
+ - tests: use printf instead of echo where we need portability
+ - tests: rename and generalize basic-binaries to test-snapd-tools
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 26 Jul 2016 15:49:04 +0200
+
+snapd (2.0.10) xenial; urgency=medium
+
+ * New upstream release: LP: #1597329
+ - interfaces: also allow @{PROC}/@{pid}/mountinfo and
+ @{PROC}/@{pid}/mountstats
+ - interfaces: allow read access to /etc/machine-id and
+ @{PROC}/@{pid}/smaps
+ - interfaces: miscelleneous policy updates for default, log-observe
+ and system-observe
+ - snapstate: add logging after a successful doLinkSnap
+ - tests, integration-tests: port try tests to spread
+ - store, cmd/snapd: send a basic user-agent to the store
+ - store: add buy method
+ - client: retry on failed GETs
+ - tests: actual refresh test
+ - docs: REST API update
+ - interfaces: add mount support for hooks.
+ - interfaces: add udev support for hooks.
+ - interfaces: add dbus support for hooks.
+ - tests, integration-tests: port refresh test to spread
+ - tests, integration-tests: port change errors test to spread
+ - overlord/ifacestate: don't retry snap security setup
+ - integration-tests: remove unused file
+ - tests: manage the socket unit when reseting state
+ - overlord: improve organization of state patches
+ - tests: wait for snapd listening after reset
+ - interfaces/builtin: allow other sr*/scd* optical devices
+ - systemd: add support for squashfuse
+ - snap: make snaps vanishing less fatal for the system
+ - snap-exec: os.Exec() needs argv0 in the args[] slice too
+ - many: add new `create-user` command
+ - interfaces: auto-connect content interfaces with the same content
+ and developer
+ - snapstate: add Current revision to SnapState
+ - readme: tweak readme blurb
+ - integration-tests: wait for listening port instead of active
+ service reported by systemd
+ - many: rename Current -> {CurrentSideInfo,CurrentInfo}
+ - spread: fix home interface test after suite move
+ - many: name unversioned data.
+ - interfaces: add "content" interface
+ - overlord/snapstate: defaultBackend can go away now
+ - debian: comment to remember why the timer is setup like it is
+ - tests,spread.yaml: introduce an upgrade test, support/split into
+ two suites for this
+ - overlord,overlord/snapstate: ensure we keep snap type in snapstate
+ of each snap
+ - many: rework the firstboot support
+ - integration-tests: fix test failure
+ - spread: keep core on suite restore
+ - tests: temporary fix for state reset
+ - overlord: add infrastructure for simple state format/content
+ migrations
+ - interfaces: add seccomp support for hooks.
+ - interfaces: allow gvfs shares in home and temporarily allow
+ socketcall by default (LP: #1592901, LP: #1594675)
+ - tests, integration-tests: port network-bind interface tests to
+ spread
+ - snap,snap/snaptest: use PopulateDir/MakeTestSnapWithFiles directly
+ and remove MockSnapWithHooks
+ - interfaces: add mpris interface
+ - tests: enable `snap run` on i386
+ - tests, integration-tests: port network interface test to spread
+ - tests, integration-tests: port interfaces cli to spread
+ - tests, integration-tests: port leftover install tests to spread
+ - interfaces: add apparmor support for hooks.
+ - tests, integration-tests: port log-observe interface tests to
+ spread
+ - asserts: improve Decode doc comment about assertion format
+ - tests: moved snaps to lib
+ - many: add the camera interface
+ - many: add optical-drive interface
+ - interfaces: auto-connect home if running on classic
+ - spread: bump gccgo test timeout
+ - interfaces: use security tags to index security snippets.
+ - daemon, overlord/snapstate, store: send confinement header to the
+ store for install
+ - spread: run tests on 16.04 i386 concurrently
+ - tests,integration-tests: port install error tests to spread
+ - interfaces: add a serial-port interface
+ - tests, integration-tests, debian: port sideload install tests to
+ spread
+ - interfaces: add new bind security backend and refactor
+ backendtests
+ - snap: load and validate implicit hooks.
+ - tests: add a build/run test for gccgo in spread
+ - cmd/snap/cmd_login: Adjust message after adding support for wheel
+ group
+ - tests, integration-tests: ported install from store tests to
+ spread
+ - snap: make `snap change <taskid>` show task progress
+ - tests, integration-tests: port search tests to spread
+ - overlord/state,daemon: make abort proceed immediately, fix doc
+ comment, improve tests
+ - daemon: extend privileged access to users in "wheel" group
+ - snap: tweak `snap refresh` and `snap refresh --list` outputTiny
+ branch that does three things:
+ - interfaces: refactor auto-connection candidate check
+ - snap: add support for snap {install,refresh}
+ --{edge,beta,candidate,stable}
+ - release: don't force KDE Neon into devmode.
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 29 Jun 2016 21:02:39 +0200
+
+snapd (2.0.9) xenial; urgency=medium
+
+ * New upstream release: LP: #1593201
+ - snap: add the magic redirect part of `snap run`
+ - tests, integration-tests: port server related tests to spread
+ - overlord/snapstate: log restarting in the task
+ - daemon: test restart wiring, fix setup/teardown
+ - cmd: don't show the price if a snap has already been purchased
+ - tests, integration-tests: port listing tests to spread
+ - integration-tests: do not try to kill ubuntu-clock-app.clock (no
+ longer a process)
+ - several: tie up overlord's restart handler into daemon; adjust
+ snap to cope
+ - tests, integration-tests: port abort tests to spread
+ - integration-tests: fix flaky TestRemoveBusyRetries
+ - testutils: refactor/mock exec
+ - snap,cmd: add hook support to snap run.
+ - overlord/snapstate: remove Download from backend
+ - store: use a custom logging transport
+ - overlord/hookstate: implement basic HookManager.
+ - spread: move the suite restore to restore-each
+ - asserts: turn model os into model core field, making it also more
+ like the kernel and gadget fields
+ - asserts: / is not allowed in primary key headers, follow the store
+ in this
+ - release: enable full confinement on Elementary 0.4
+ - integration-tests: fix another i386 autopkgtest failure.
+ - cmd/snap: create SNAP_USER_DATA and common dirs in `snap run`
+ - many: have the installation of the core snap request a restart (on
+ classic)
+ - asserts: allow to load also account assertions into the trusted
+ set
+ - many: install snaps in devmode on distributions without complete
+ apparmor and seccomp support
+ - spread: run on travis
+ - snapenv: do not hardcode amd64 in tests
+ - spread: initial harness and first test
+ - interfaces: miscelleneous policy updates for chromium, x86,
+ opengl, etc
+ - integration-tests: remove daemon to use the log-observe interface
+ - client: remove client.Revision and import snap.Revision instead
+ - integration-tests: wait for network-bind service in try test
+ - many: move over from snappy to snapstate/backend SetupSnap and
+ related code
+ - integration-tests: add interfaces cli tests
+ - snapenv: cleanup snapenv.{Basic,User}
+ - cmd/snap: also print slots that connect to the wanted snap (LP:
+ #1590704)
+ - asserts: error style, use "cannot" instead of "failed to"
+ following the main decided style
+ - integration-tests: wait until the network-bind service is up
+ before testing
+ - many: add new `snap run` command
+ - snappy: unexport snappy.Install and snappy.Overlord.{Un,}Install
+ - many: add some shared testing helpers to snap/snaptest and to
+ boot/boottest
+ - rest-api: support to send apps per snap (LP: #1564076)
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 16 Jun 2016 13:56:12 +0200
+
+snapd (2.0.8.1) UNRELEASED; urgency=medium
+
+ * New upstream release
+ - Cherry pick four commits that show snaps as installed in devmode on
+ distributions without full confinement dependencies available:
+
+ 25634d3364a46b5e9147e4466932c59b1b572d35
+ 53f2e8d5f1b2d7ce13f5b50be4c09fa1de8cf1e0
+ 38771f4cc324ad9dd4aa48b03108d13a2c361aad
+ c46e069351c61e45c338c98ab12689a319790bd5
+
+ -- Zygmunt Krynicki <zygmunt.krynicki@canonical.com> Tue, 14 Jun 2016 15:55:30 +0200
+
+snapd (2.0.8) xenial; urgency=medium
+
+ * New upstream release: LP: #1589534
+ - debian: make `snap refresh` times more random (LP: #1537793)
+ - cmd: ExecInCoreSnap looks in "core" snap first, and only in
+ "ubuntu-core" snap if rev>125.
+ - cmd/snap: have 'snap list' display helper message on stderr
+ (LP: #1587445)
+ - snap: make app names more restrictive.
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 08 Jun 2016 07:56:58 +0200
+
+snapd (2.0.7) xenial; urgency=medium
+
+ * New upstream release: LP: #1589534
+ - debian: do not ship /etc/ld.so.conf.d/snappy.conf (LP: #1589006)
+ - debian: fix snapd.refresh.service install and usage (LP: #1588977)
+ - ovlerlord/state: actually support task setting themself as
+ done/undone
+ - snap: do not use "." import in revision_test.go, as this breaks
+ gccgo-6 (fix build failure on powerpc)
+ - interfaces: add fcitx and mozc input methods to unity7
+ - interfaces: add global gsettings interfaces
+ - interfaces: autoconnect home and doc updates (LP: #1588886)
+ - integration-tests: remove
+ abortSuite.TestAbortWithValidIdInDoingStatus
+ - many: adding backward compatible code to upgrade SnapSetup.Flags
+ - overlord/snapstate: handle sideloading over an old sideloaded snap
+ without panicing
+ - interfaces: add socketcall() to the network/network-bind
+ interfaces (LP: #1588100)
+ - overlord/snapstate,snappy: move over CanRemoveThis moves over the
+ CanRemove check to snapstate itself.overlord/snapstate
+ - snappy: move over CanRemove
+ - overlord/snapstate,snappy: move over CopyData and Remove*Data code
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 06 Jun 2016 16:35:50 +0200
+
+snapd (2.0.6) xenial; urgency=medium
+
+ * New upstream release: LP: #1588052:
+ - many: repository moved to snapcore/snapd
+ - debian: add transitional pkg for the github location change
+ - snap: ensure `snap try` work with relative paths
+ - debian: drop run/build dependency on lsb-release
+ - asserts/tool: gpg key pair manager
+ - many: add new snap-exec
+ - many: implement `snap refresh --list` and `snap refresh`
+ - snap: add parsing support for hooks.
+ - many: add the cups interface
+ - interfaces: misc policy fixes (LP: #1583794)
+ - many: add `snap try`
+ - interfaces: allow using sysctl and scmp_sys_resolver for parsing
+ kernel logs
+ - debian: make snapd get its environ from /etc/environment
+ - daemon,client,snap: revisions are now strings
+ - interfaces: allow access to new ibus abstract socket path
+ LP: #1580463
+ - integration-tests: add remove tests
+ - asserts: stronger crypto choices and follow better latest designs
+ - snappy,daemon: hollow out more of snappy (either removing or not
+ exporting stuff on its way out), snappy/gadget.go is gone
+ - asserts: rename device-serial to serial
+ - asserts: rename identity to account (and username access)
+ - integration-tests: add changes tests
+ - backend: add tests for environment wrapper generation
+ - interfaces/builtin: add location-control interface
+ - overlord/snapstate: move over check snap logic from snappy
+ - release: use os-release instead of lsb-release for cross-distro
+ use
+ - asserts: allow empty snap-name for snap-declaration
+ - interfaces/builtin,docs,snap: add the pulseaudio interface
+ - many: add support for an environment map inside snap.yaml
+ - overlord/snapstate: increase robustness of doLinkSnap/undoLinkSnap
+ with sanity unit tests
+ - snap: parse epoch property
+ - snappy: do nothing in SetNextBoot when running on classic
+ - snap: validate snap type
+ - integration-tests: extend find command tests
+ - asserts: extend tests to cover mandatory and empty headers
+ - tests: stop the update-pot check in run-checks
+ - snap: parse confinement property.
+ - store: change applyUbuntuStoreHeaders to not take accept, and to
+ take a channel
+ - many: struct-based revisions, new representation
+ - interfaces: remove 'audit deny' rules from network_control.go
+ - interfaces: add com.canonical.UrlLauncher.XdgOpen to unity7
+ interface
+ - interfaces: firewall-control can access xtables lock file
+ - interfaces: allow unity7 AppMenu
+ - interfaces: allow unity7 launcher API
+ - interfaces/builtin: add location-observe interface
+ - snap: fixed snap empty list text LP: #1587445
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 02 Jun 2016 08:23:50 +0200
+
+snapd (2.0.5) xenial; urgency=medium
+
+ * New upstream release: LP: #1583085
+ - interfaces: add dbusmenu, freedesktop and kde notifications to
+ unity7 (LP: #1573188)
+ - daemon: make localSnapInfo return SnapState
+ - cmd: make snap list with no snaps not special
+ - debian: workaround for XDG_DATA_DIRS issues
+ - cmd,po: fix conflicts, apply review from #1154
+ - snap,store: load and store the private flag sent by the store in
+ SideInfo
+ - interfaces/apparmor/template.go: adjust /dev/shm to be more usable
+ - store: use purchase decorator in Snap and FindSnaps
+ - interfaces: first version of the networkmanager interface
+ - snap, snappy: implement the new (minmimal) kernel spec
+ - cmd/snap, debian: move manpage generation to depend on an environ
+ key; also, fix completion
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 19 May 2016 15:29:16 +0200
+
+snapd (2.0.4) xenial; urgency=medium
+
+ * New upstream release:
+ - interfaces: cleanup explicit denies
+ - integration-tests: remove the ancient integration daemon tests
+ - integration-tests: add network-bind interface test
+ - integration-tests: add actual checks for undoing install
+ - integration-tests: add store login test
+ - snap: add certain implicit slots only on classic
+ - integration-tests: add coverage flags to snapd.service ExecStart
+ setting when building from branch
+ - integration-tests: remove the tests for features removed in 16.04.
+ - daemon, overlord/snapstate: "(de)activate" is no longer a thing
+ - docs: update meta.md and security.md for current snappy
+ - debian: always start snapd
+ - integration-tests: add test for undoing failed install
+ - overlord: handle ensureNext being in the past
+ - overlord/snapstate,overlord/snapstate/backend,snappy: start
+ backend porting LinkSnap and UnlinkSnap
+ - debian/tests: add reboot capability to autopkgtest and execute
+ snapPersistsSuite
+ - daemon,snappy,progress: drop license agreement broken logic
+ - daemon,client,cmd/snap: nice access denied message
+ (LP: #1574829)
+ - daemon: add user parameter to all commands
+ - snap, store: rework purchase methods into decorators
+ - many: simplify release package and add OnClassic
+ - interfaces: miscellaneous policy updates
+ - snappy,wrappers: move desktop files handling to wrappers
+ - snappy: remove some obviously dead code
+ - interfaces/builtin: quote apparmor label
+ - many: remove the gadget yaml support from snappy
+ - snappy,systemd,wrappers: move service units generation to wrappers
+ - store: add method to determine if a snap must be bought
+ - store: add methods to read purchases from the store
+ - wrappers,snappy: move binary wrapper generation to new package
+ wrappers
+ - snap: add `snap help` command
+ - integration-tests: remove framework-test data and avoid using
+ config-snap for now
+ - add integration test to verify fix for LP: #1571721
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 13 May 2016 17:19:37 -0700
+
+snapd (2.0.3) xenial; urgency=medium
+
+ * New upstream micro release:
+ - integration-tests, debian/tests: add unity snap autopkg test
+ - snappy: introduce first feature flag for assumes: common-data-dir
+ - timeout,snap: add YAML unmarshal function for timeout.Timeout
+ - many: go into state.Retry state when unmounting a snap fails.
+ (LP: #1571721, #1575399)
+ - daemon,client,cmd/snap: improve output after snap
+ install/refresh/remove (LP: #1574830)
+ - integration-tests, debian/tests: add test for home interface
+ - interfaces,overlord: support unversioned data
+ - interfaces/builtin: improve the bluez interface
+ - cmd: don't include the unit tests when building with go test -c
+ for integration tests
+ - integration-tests: teach some new trick to the fake store,
+ reenable the app refresh test
+ - many: move with some simplifications test snap building to
+ snap/snaptest
+ - asserts: define type for revision related errors
+ - snap/snaptest,daemon,overlord/ifacestate,overlord/snapstate: unify
+ mocking snaps behind MockSnap
+ - snappy: fix openSnapFile's handling of sideInfo
+ - daemon: improve snap sideload form handling
+ - snap: add short and long description to the man-page
+ (LP: #1570280)
+ - snappy: remove unused SetProperty
+ - snappy: use more accurate test data
+ - integration-tests: add a integration test about remove removing
+ all revisions
+ - overlord/snapstate: make "snap remove" remove all revisions of a
+ snap (LP: #1571710)
+ - integration-tests: re-enable a bunch of integration tests
+ - snappy: remove unused dbus code
+ - overlord/ifacestate: fix setup-profiles to use new snap revision
+ for setup (LP: #1572463)
+ - integration-tests: add regression test for auth bug LP:#1571491
+ - client, snap: remove obsolete TypeCore which was used in the old
+ SystemImage days
+ - integration-tests: add apparmor test
+ - cmd: don't perform type assertion when we know error to be nil
+ - client: list correct snap types
+ - intefaces/builtin: allow getsockname on connected x11 plugs
+ (LP: #1574526)
+ - daemon,overlord/snapstate: read name out of sideloaded snap early,
+ improved change summary
+ - overlord: keep tasks unlinked from a change hidden, prune them
+ - integration-tests: snap list on fresh boot is good again
+ - integration-tests: add partial term to the find test
+ - integration-tests: changed default release to 16
+ - integration-tests: add regression test for snaps not present after
+ reboot
+ - integration-tests: network interface
+ - integration-tests: add proxy related environment variables to
+ snapd env file
+ - README.md: snappy => snap
+ - etc: trivial typo fix (LP:#1569892)
+ - debian: remove unneeded /var/lib/snapd/apparmor/additional
+ directory (LP: #1569577)
+ - builtin/unity7.go: allow using gmenu. LP: #1576287
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 03 May 2016 07:51:57 +0200
+
+snapd (2.0.2) xenial; urgency=medium
+
+ * New upstream release:
+ - systemd: add multi-user.target (LP: #1572125)
+ - release: our series is 16
+ - integration-tests: fix snapd binary path for mounting the daemon
+ built from branch
+ - overlord,snap: add firstboot state sync
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 19 Apr 2016 16:02:44 +0200
+
+snapd (2.0.1) xenial; urgency=medium
+
+ * client,daemon,overlord: fix authentication:
+ - fix incorrect authenication check (LP: #1571491)
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 18 Apr 2016 07:24:33 +0200
+
+snapd (2.0) xenial; urgency=medium
+
+ * New upstream release:
+ - debian: put snapd in /usr/lib/snapd/
+ - cmd/snap: minor polishing
+ - cmd,client,daemon: add snap abort command
+ - overlord: don't hold locks when callling backends
+ - release,store,daemon: no more default-channel, release=>series
+ - many: drop support for deprecated environment variables
+ (SNAP_APP_*)
+ - many: support individual ids in changes cmd
+ - overlord/state: use numeric change and task ids
+ - overlord/auth,daemon,client,cmd/snap: logout
+ - daemon: don't install ubuntu-core twice
+ - daemon,client,overlord/state,cmd: add changes command
+ - interfaces/dbus: drop superfluous backslash from template
+ - daemon, overlord/snapstate: updates are users too!
+ - cmd/snap,daemon,overlord/ifacestate: add support for developer
+ mode
+ - daemon,overlord/snapstate: on refresh use the remembered channel,
+ default to stable channel otherwise
+ - cmd/snap: improve UX of snap interfaces when there are no results
+ - overlord/state: include time in task log messages
+ - overlord: prune and abort old changes and tasks
+ - overlord/ifacestate: add implicit slots in setup-profiles
+ - daemon,overlord: setup authentication for store downloads
+ - daemon: macaroon-authed users are like root, and sudoers can login
+ - daemon,client,docs: send install options to daemon
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Sat, 16 Apr 2016 22:15:40 +0200
+
+snapd (1.9.4) xenial; urgency=medium
+
+ * New upstream release:
+ - etc: fix desktop file location
+ - overlord/snapstate: stop an update once download sees the revision
+ is already installed
+ - overlord: make SnapState.DevMode a method, store flags
+ - snappy: no more snapYaml in snappy.Snap
+ - daemon,cmd,dirs,lockfile: drop all lockfiles
+ - debian: use sudo in setup of the proxy environment
+ - snap/snapenv,snappy,systemd: expose SNAP_REVISION to app
+ environment
+ - snap: validate similarly to what we did with old snapYaml info
+ from squashfs snaps
+ - daemon,store: plug in authentication for store search/details
+ - overlord/snapstate: fix JSON name of SnapState.Candidate
+ - overlord/snapstate: start using revisions higher than 100000 for
+ local installs (sideloads)
+ - interfaces,overlorf/ifacestate: honor user choice and don't auto-
+ connect disconnected plugs
+ - overlord/auth,daemon,client: hide user ids again
+ - daemon,overlord/snapstate: back /snaps (and so snap list) using
+ state
+ - daemon,client,overlord/auth: rework state auth data
+ - overlord/snapstate: disable Activate and Deactivate
+ - debian: fix silly typo in autopkgtest setup
+ - overlord/ifacestate: remove connection state with discard-conns
+ task, on the removal of last snap
+ - daemon,client: rename API update action to refresh
+ - cmd/snap: rework login to be more resilient
+ - overlord/snapstate: deny two changes on one snap
+ - snappy: fix crash on certain snap.yaml
+ - systemd: use native systemctl enable instead of our own
+ implementation
+ - store: add workaround for misbehaving store
+ - debian: make autopkgtest use the right env vars
+ - state: log do/undo status too when a task is run
+ - docs: update rest.md with price information
+ - daemon: only include price property if the snap is non-free
+ - daemon, client, cmd/snap: connect/disconnect now async
+ - snap,snappy: allow snaps to require system features
+ - integration-tests: fix report of skips in SetUpTest method
+ - snappy: clean out major bits (still using Installed) now
+ unreferenced as cmd/snappy is gone
+ - daemon/api,overlord/auth: add helper to get UserState from a
+ client request
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 15 Apr 2016 23:30:00 +0200
+
+snapd (1.9.3) xenial; urgency=medium
+
+ * New upstream release:
+ - many: prepare for opengl support on classic
+ - interfaces/apparmor: load all apparmor profiles on snap setup
+ - daemon,client: move async resource to change in meta
+ - debian: disable autopilot
+ - snap: add basic progress reporting
+ - client,cmd,daemon,snap,store: show the price of snaps in the cli
+ - state: add minimal taskrunner logging
+ - daemon,snap,overlord/snapstate: in the API get the snap icon using
+ state
+ - client,daemon,overlord: don't guess snap file vs. name
+ - overlord/ifacestate: reload snap connections when setting up
+ security for a given snap
+ - snappy: remove cmd/snappy (superseded in favour of cmd/snap)
+ - interfaecs/apparmor: remove all traces of old-security from
+ apparmor backend
+ - interfaces/builtin: add bluez interface
+ - overlord/ifacestate: don't crash if connection cannot be reloaded
+ - debian: add searchSuite to autopkgtest
+ - client, daemon, cmd/snap: no more tasks; everything is changes
+ - client: send authorization header in client requests
+ - client, daemon: marshal suggested currency over REST
+ - docs, snap: enumerate snap types correctly in docs and comments
+ - many: add store authenticator parameter
+ - overlord/ifacestate,daemon: setup security on conect and
+ disconnect
+ - interfaces/apparmor: remove unused apparmor variables
+ - snapstate: add missing "TaskProgressAdapter.Write()" for working
+ progress reporting
+ - many: clean out snap config related code not for OS
+ - daemon,client,cmd: return snap list from /v2/snaps
+ - docs: update `/v2/snaps` endpoint documentation
+ - interfaces: rename developerMode to devMode
+ - daemon,client,overlord: progress current => done
+ - daemon,client,cmd/snap: move query metadata to top-level doc
+ - interfaces: add TestSecurityBackend
+ - many: replace typographic quotes with ASCII
+ - client, daemon: rework rest changes to export "ready" and "err"
+ - overlord/snapstate,snap,store: track snap-id in side-info and
+ therefore in state
+ - daemon: improve mocking of interfaces API tests
+ - integration-tests: remove origins in default snap names for udf
+ call
+ - integration-test: use "snap list" in GetCurrentVersion
+ - many: almost no more NewInstalledSnap reading manifest from
+ snapstate and backend
+ - daemon: auto install ubuntu-core if missing
+ - oauth,store: remove OAuth authentication logic
+ - overlord/ifacestate: simplify some tests with implicit manager
+ initialization
+ - store, snappy: move away from hitting details directly
+ - overlord/ifacestate: reload connections when restarting the
+ manager
+ - overlord/ifacestate: increase flexibility of unit tests
+ - overlord: use state to discover all installed snaps
+ - overlord/ifacestate: track connections in the state
+ - many: separate copy-data from unlinking of current snap
+ - overlord/auth,store/auth: add macaroon authenticator to UserState
+ - client: support for /v2/changes and /v2/changes/{id}
+ - daemon/api,overlord/auth: rework authenticated users information
+ in state
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 14 Apr 2016 23:29:43 +0200
+
+snapd (1.9.2) xenial; urgency=medium
+
+ * New upstream release:
+ - cmd/snap,daemon,store: rework login command to use daemon login
+ API
+ - store: cache suggested currency from the store
+ - overlord/ifacestate: modularize and extend tests
+ - integration-tests: reenable failure tests
+ - daemon: include progress in rest changes
+ - daemon, overlord/state: expose individual changes
+ - overlord/ifacestate: drop duplicate package comment
+ - overlord/ifacestate: allow tests to override security backends
+ - cmd/snap: install *.snap and *.snap.* as files too
+ - interfaces/apparmor: replace /var/lib/snap with /var/snap
+ - daemon,overlord/ifacestate: connect REST API to interfaces in the
+ overlord
+ - debian: remove unneeded dependencies from snapd
+ - overlord/state: checkpoint on final progress only
+ - osutil: introduce IsUIDInAny
+ - overlord/snapstate: rename GetSnapState to Get, SetSnapState to
+ Set
+ - daemon: add id to changes json
+ - overlord/snapstate: SetSnapState() needs locks
+ - overlord: fix broken tests
+ - overlord/snapstate,overlord/ifacestate: reimplement SnapInfo (as
+ Info) actually using the state
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 13 Apr 2016 17:27:00 +0200
+
+snapd (1.9.1.1) xenial; urgency=medium
+
+ * debian/tests/control:
+ - add git to make autopkgtest work
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 12 Apr 2016 17:19:19 +0200
+
+snapd (1.9.1) xenial; urgency=medium
+
+ * Add warning about installing ubuntu-core-snapd-units on Desktop systems.
+ * Add ${misc:Depends} to ubuntu-core-snapd-units.
+ * interfaces,overlord: add support for auto-connecting plugs on
+ install
+ * fix sideloading snaps and (re)add tests for this
+ * add `ca-certificates` to the test-dependencies to fix autopkgtest
+ failure on armhf
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 12 Apr 2016 14:39:57 +0200
+
+snapd (1.9) xenial; urgency=medium
+
+ * rename source and binary package to "snapd"
+ * update directory layout to final 16.04 layout
+ * use `snap` command instead of the previous `snappy`
+ * use `interface` based security
+ * use new state engine for install/update/remove
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 12 Apr 2016 01:05:09 +0200
+
+ubuntu-snappy (1.7.3+20160310ubuntu1) xenial; urgency=medium
+
+ - debian: update versionized ubuntu-core-launcher dependency
+ - debian: tweak desktop file dir, ship Xsession.d snip for seamless
+ integration
+ - snappy: fix hw-assign to work with per-app udev tags
+ - snappy: use $snap.$app as per-app udev tag
+ - snap,snappy,systemd: %s/\<SNAP_ORIGIN\>/SNAP_DEVELOPER/g
+ - snappy: add mksquashfs --no-xattrs parameter
+ - snap,snappy,systemd: kill SNAP_FULLNAME
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 10 Mar 2016 09:26:20 +0100
+
+ubuntu-snappy (1.7.3+20160308ubuntu1) xenial; urgency=medium
+
+ - snappy,snap: move icon under meta/gui/
+ - debian: add snap.8 manpage
+ - debian: move snapd to /usr/lib/snappy/snapd
+ - snap,snappy,systemd: remove TMPDIR, TEMPDIR, SNAP_APP_TMPDIR
+ - snappy,dirs: add support to use desktop files from inside snaps
+ - daemon: snapd API events endpoint redux
+ - interfaces/builtin: add "network" interface
+ - overlord/state: do small fixes (typo, id clashes paranoia)
+ - overlord: add first pass of the logic in StateEngine itself
+ - overlord/state: introduce Status/SetStatus on Change
+ - interfaces: support permanent security snippets
+ - overlord/state: introduce Status/SetStatus and
+ Progress/SetProgress on Task
+ - overlord/state: introduce Task and Change.NewTask
+ - many: selectively swap semantics of plugs and slots
+ - client,cmd/snap: remove useless indirection in Interfaces
+ - interfaces: maintain Plug and Slot connection details
+ - client,daemon,cmd/snap: change POST /2.0/interfaces to work with
+ lists
+ - overlord/state: introduce Change and NewChange on state to create
+ them
+ - snappy: bugfix for snap.yaml parsing to be more consistent with
+ the spec
+ - snappy,systemd: remove "ports" from snap.yaml
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 08 Mar 2016 11:24:09 +0100
+
+ubuntu-snappy (1.7.3+20160303ubuntu4) xenial; urgency=medium
+
+ * rename:
+ debian/golang-snappy-dev.install ->
+ debian/golang-github-ubuntu-core-snappy-dev.install:
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 03 Mar 2016 12:29:16 +0100
+
+ubuntu-snappy (1.7.3+20160303ubuntu3) xenial; urgency=medium
+
+ * really fix typo in dependency name
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 03 Mar 2016 12:21:39 +0100
+
+ubuntu-snappy (1.7.3+20160303ubuntu2) xenial; urgency=medium
+
+ * fix typo in dependency name
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 03 Mar 2016 12:05:36 +0100
+
+ubuntu-snappy (1.7.3+20160303ubuntu1) xenial; urgency=medium
+
+ - debian: update build-depends for MIR
+ - many: implement new REST API: GET /2.0/interfaces
+ - integration-tests: properly stop snapd from branch
+ - cmd/snap: update tests for go-flags changes
+ - overlord/state: implement Lock/Unlock with implicit checkpointing
+ - overlord: split out the managers and State to their own
+ subpackages of overlord
+ - snappy: rename "migration-skill" to "old-security" and use new
+ interface names instead of skills
+ - client,cmd/snap: clarify name ambiguity in Plug or Slot
+ - overlord: start working on state engine along spec v2, have the
+ main skeleton follow that
+ - classic, oauth: update tests for change in MakeRandomString()
+ - client,cmd/snap: s/add/install/:-(
+ - interfaces,daemon: specialize Name to either Plug or Slot
+ - interfaces,interfaces/types: unify security snippet functions
+ - snapd: close the listener on Stop, to force the http.Serve loop to
+ exit
+ - snappy,daemon,snap/lightweight,cmd/snappy,docs/rest.md: expose
+ explicit channel selection to rest api
+ - interfaces,daemon: rename package holding built-in interfaces
+ - integration-tests: add the first classic dimension tests
+ - client,deaemon,docs: rename skills to interfaces on the wire
+ - asserts: add identity assertion type
+ - integration-tests: add the no_proxy env var
+ - debian: update build-depends for new package names
+ - oauth: fix oauth & quoting in the oauth_signature
+ - integration-tests: remove unused field
+ - integration-tests: add the http proxy argument
+ - interfaces,interfaces/types,deamon: mass internal rename to
+ interfaces
+ - client,cmd/snap: rename skills to interfaces (part 2)
+ - arch: fix missing mapping for powerpc
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 03 Mar 2016 11:00:19 +0100
+
+ubuntu-snappy (1.7.3+20160225ubuntu1) xenial; urgency=medium
+
+ - integration-tests: always use the built snapd when compiling
+ binaries from branch
+ - cmd/snap: rename skills to interfaces
+ - testutil,skills/types,skills,daemon: tweak discovery of know skill
+ types
+ - docs: add docs for arm64 cross building
+ - overlord: implement basic ReadState/WriteState
+ - overlord: implement Get/Set/Copy on State
+ - integration-tests: fix dd output check
+ - integration-tests: add fromBranch config field
+ - integration-tests: use cli pkg methods in hwAssignSuite
+ - debian: do not create the snappypkg user, we don't need it anymore
+ - arch: fix build failure on s390x
+ - classic: cleanup downloaded lxd tarball
+ - cmd/snap,client,integration-tests: rename snap subcmds
+ 'assert'=>'ack', 'asserts'=>'known'
+ - skills: fix broken tests builds
+ - skills,skills/types: pass slot to SlotSecuritySnippet()
+ - skills/types: teach bool-file about udev security
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 25 Feb 2016 16:17:19 +0100
+
+ubuntu-snappy (1.7.2+20160223ubuntu1) xenial; urgency=medium
+
+ * New git snapshot:
+ - asserts: introduce snap-declaration
+ - cmd/snap: fix integration tests for the "cmd_asserts"
+ - integration-tests: fix fanctl output check
+ - cmd/snap: fix test failure after merging 23a64e6
+ - cmd/snap: replace skip-help with empty description
+ - docs: update security.md to match current migration-skill
+ semantics
+ - snappy: treat commands with 'daemon' field as services
+ - asserts: use more consistent names for receivers in
+ snap_asserts*.go
+ - debian: add missing golang-websocket-dev build-dependency
+ - classic: if classic fails to get created, undo the bind mounts
+ - snappy: never return nil in NewLocalSnapRepository()
+ - notifications: A simple notification system
+ - snappy: when using staging, authenticate there instead
+ - integration-tests/snapd: fix the start of the test snapd socket
+ - skills/types: use CamelCase for security names
+ - skills: add support for implicit revoke
+ - skills: add security layer
+ - integration-tests: use exec.Command wrapper for updates
+ - cmd/snap: add 'snap skills'
+ - cms/snap: add 'snap revoke'
+ - docs: add docs for skills API
+ - cmd/snap: add 'snap grant'
+ - cmd/snappy, coreconfig, daemon, snappy: move config to always be
+ bytes (in and out)
+ - overlord: start with a skeleton and stubs for Overlord,
+ StateEngine, StateJournal and managers
+ - integration-tests: skip tests affected by LP: #1544507
+ - skills/types: add bool-file
+ - po: refresh translation templates
+ - cmd/snap: add 'snap experimental remove-skill-slot'
+ - asserts: introduce device assertion
+ - cmd/snap: implemented add, remove, purge, refresh, rollback,
+ activate, deactivate
+ - cmd/snap: add 'snap experimental add-skill-slot'
+ - cmd/snap: add 'snap experimental remove-skill'
+ - cmd/snap: add tests for common skills code
+ - cmd/snap: add 'snap experimental add-skill'
+ - asserts: make assertion checkers used by db.Check modular and
+ pluggable
+ - cmd,client,daemon,caps,docs,po: remove capabilities
+ - scripts: move the script to get dependencies to a separate file
+ - asserts: make the disk layout compatible for storing more than one
+ revision
+ - cmd/snap: make the assert command options exported
+ - integration-tests: Remove the target release and channel
+ - asserts: introduce model assertion
+ - integration-tests: add exec.Cmd wrapper
+ - cmd/snap: add client test support methods
+ - cmd/snap: move key=value attribute parsing to commmon
+ - cmd/snap: apply new style consistency to "snap" commands.
+ - cmd/snap: support redirecting the client for testing
+ - cmd/snap: support testing command output
+ - snappy,daemon: remove the meta repositories abstractions
+ - cmd: add support for experimental commands
+ - cmd/snappy,daemon,snap,snappy: remove SetActive from parts
+ - cmd/snappy,daemon,snappy,snap: remove config from parts interface
+ - client: improve test data
+ - cmd: allow to construct a fresh parser
+ - cmd: don't treat help as an error
+ - cmd/snappy,snappy: remove "Details" from the repository interface
+ - asserts: check that primary keys are set when
+ Decode()ing/assembling assertions
+ - snap,snappy: refactor to remove "Install" from the Part interface
+ - client,cmd: make client.New() configurable
+ - client: enable retrieving asynchronous operation information with
+ `Client.Operation`.
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 23 Feb 2016 11:28:18 +0100
+
+ubuntu-snappy (1.7.2+20160204ubuntu1) xenial; urgency=medium
+
+ * New git snapshot:
+ - integration-tests: fix the rollback error messages
+ - integration-test: use the common cli method when trying to install
+ an unexisting snap
+ - integration-tests: rename snap find test
+ - daemon: refactor makeErrorResponder()
+ - integration: add regression test for LP: #1541317
+ - integration-tests: reenable TestRollbackMustRebootToOtherVersion
+ - asserts: introduce "snap asserts" subcmd to show assertions in the
+ system db
+ - docs: fix parameter style
+ - daemon: use underscore in JSON interface
+ - client: add skills API
+ - asserts,docs/rest.md: change Encoder not to add extra newlines at
+ the end of the stream
+ - integration-tests: "snappy search" is no more, its "snap search"
+ now
+ - README, integration-tests/tests: chmod snapd.socket after manual
+ start.
+ - snappy: add default security profile if none is specified
+ - skills,daemon: add REST APIs for skills
+ - cmd/snap, cmd/snappy: move from `snappy search` to `snap find`.
+ - The first step towards REST world domination: search is now done
+ via
+ - debian: remove obsolete /etc/grub.d/09_snappy on upgrade
+ - skills: provide different security snippets for skill and slot
+ side
+ - osutil: make go vet happy again
+ - snappy,systemd: use Type field in systemd.ServiceDescription
+ - skills: add basic grant-revoke methods
+ - client,daemon,asserts: expose the ability to query assertions in
+ the system db
+ - skills: add basic methods for slot handling
+ - snappy,daemon,snap: move "Uninstall" into overlord
+ - snappy: move SnapFile.Install() into Overlord.Install()
+ - integration-tests: re-enable some failover tests
+ - client: remove snaps
+ - asserts: uniform searching across trusted (account keys) and main
+ backstore
+ - asserts: introduce Decoder to parse streams of assertions and
+ Encoder to build them
+ - client: filter snaps with a search query
+ - client: pass query as well as path in client internals
+ - skills: provide different security snippets for skill and slot
+ side
+ - snappy: refactor snapYaml to remove methods on snapYaml type
+ - snappy: remove unused variable from test
+ - skills: add basic methods for skill handing
+ - snappy: remove support for meta/package.yaml and implement new
+ meta/snap.yaml
+ - snappy: add new overlord type responsible for
+ Installed/Install/Uninstall/SetActive and stub it out
+ - skills: add basic methods for type handling
+ - daemon, snappy: add find (aka search)
+ - client: filter snaps by type
+ - skills: tweak valid names and error messages
+ - skills: add special skill type for testing
+ - cmd/snapd,daemon: filter snaps by type
+ - partition: remove obsolete uEnv.txt
+ - skills: add Type interface
+ - integration-tests: fix the bootloader path
+ - asserts: introduce a memory backed assertion backstore
+ - integration-tests: get name of OS snap from bootloader
+ - cmd/snapd,daemon: filter snaps by source
+ - asserts,daemon: bump some copyright years for things that have
+ been touched in the new year
+ - skills: add the initial Repository type
+ - skills: add a name validation function
+ - client: filter snaps by source
+ - snappy: unmount the squashfs snap again if it fails to install
+ - snap: make a copy of the search uri before mutating it
+ Closes: LP#1537005
+ - cmd/snap,client,daemon,asserts: introduce "assert " snap
+ subcommand
+ - cmd/snappy, snappy: fix failover handling of the "active"
+ kernel/os snap
+ - daemon, client, docs/rest.md, snapd integration tests: move to the
+ new error response
+ - asserts: change Backstore interface, backstores can now access
+ primary key names from types
+ - asserts: make AssertionType into a real struct exposing the
+ metadata Name and PrimaryKey
+ - caps: improve bool-file sanitization
+ - asserts: fixup toolbelt to use exposed key ID.
+ - client: return by reference rather than by value
+ - asserts: exported filesystem backstores + explicit backstores
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 04 Feb 2016 16:35:31 +0100
+
+ubuntu-snappy (1.7.2+20160113ubuntu1) xenial; urgency=medium
+
+ * New git snapshot
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 13 Jan 2016 11:25:40 +0100
+
+ubuntu-snappy (1.7.2ubuntu1) xenial; urgency=medium
+
+ * New upstream release:
+ - bin-path integration
+ - assertions/capability work
+ - fix squashfs based snap building
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 04 Dec 2015 08:46:35 +0100
+
+ubuntu-snappy (1.7.1ubuntu1) xenial; urgency=medium
+
+ * New upstream release:
+ - fix dependencies
+ - fix armhf builds
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 02 Dec 2015 07:46:07 +0100
+
+ubuntu-snappy (1.7ubuntu1) xenial; urgency=medium
+
+ * New upstream release:
+ - kernel/os snap support
+ - squashfs snap support
+ - initial capabilities work
+ - initial assertitions work
+ - rest API support
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 18 Nov 2015 19:59:51 +0100
+
+ubuntu-snappy (1.6ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - Fix hwaccess for gpio (LP: #1493389, LP: #1488618)
+ - Fix handleAssets name normalization
+ - Run boot-ok job late (LP: #1476129)
+ - Add support for systemd socket files
+ - Add "snappy service" command
+ - Documentation improvements
+ - Many test improvements (unit and integration)
+ - Override sideload versions
+ - Go1.5 fixes
+ - Add i18n
+ - Add man-page
+ - Add .snapignore
+ - Run services that uses external ports only after the network is up
+ - Bufix in Synbootloader (LP: 1474125)
+ - Use uboot.env for boot state tracking
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 09 Sep 2015 14:20:22 +0200
+
+ubuntu-snappy (1.5ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - Use O_TRUNC when copying files
+ - Added path redefinition to include test's binaries location
+ - Don't run update-grub, instead use grub.cfg from the oem
+ package
+ - Do network configuration from first boot
+ - zero size systemd of new partition made executable to
+ prevent unrecoverable boot failure
+ - Close downloaded files
+
+ -- Ricardo Salveti de Araujo <ricardo.salveti@canonical.com> Mon, 06 Jul 2015 15:14:37 -0300
+
+ubuntu-snappy (1.4ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - Allow to run the integration tests using snappy from branch
+ - Add CopyFileOverwrite flag and behaviour to helpers.CopyFile
+ - add a bunch of missing i18n.G() now that we have gettext
+ - Generate only the translators comments that start with
+ TRANSLATORS
+ - Try both clickpkg and snappypkg when dropping privs
+
+ -- Ricardo Salveti de Araujo <ricardo.salveti@canonical.com> Thu, 02 Jul 2015 16:21:53 -0300
+
+ubuntu-snappy (1.3ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - gettext support
+ - use snappypkg user for the installed snaps
+ - switch to system-image-3.x as the system-image backend
+ - more reliable developer mode detection
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 01 Jul 2015 10:37:05 +0200
+
+ubuntu-snappy (1.2-0ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - Consider the root directory when installing and removing policies
+ - In the uboot TestHandleAssetsNoHardwareYaml, patch the cache dir
+ before creating the partition type
+ - In the PartitionTestSuite, remove the unnecessary patches for
+ defaultCacheDir
+ - Fix the help output of "snappy install -h"
+
+ -- Ricardo Salveti de Araujo <ricardo.salveti@canonical.com> Wed, 17 Jun 2015 11:42:47 -0300
+
+ubuntu-snappy (1.1.2-0ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - Remove compatibility for click-bin-path in generated exec-wrappers
+ - Release the readme.md after parsing it
+
+ -- Ricardo Salveti de Araujo <ricardo.salveti@canonical.com> Thu, 11 Jun 2015 23:42:49 -0300
+
+ubuntu-snappy (1.1.1-0ubuntu1) wily; urgency=medium
+
+ * New upstream release, including the following changes:
+ - Set all app services to restart on failure
+ - Fixes the missing oauth quoting and makes the code a bit nicer
+ - Added integrate() to set Integration to default values needed for
+ integration
+ - Moved setActivateClick to be a method of SnapPart
+ - Make unsetActiveClick a method of SnapPart
+ - Check the package.yaml for the required fields
+ - Integrate lp:snappy/selftest branch into snappy itself
+ - API to record information about the image and to check if the kernel was
+ sideloaded.
+ - Factor out update from cmd
+ - Continue updating when a sideload error is returned
+
+ -- Ricardo Salveti de Araujo <ricardo.salveti@canonical.com> Wed, 10 Jun 2015 15:54:12 -0300
+
+ubuntu-snappy (1.1-0ubuntu1) wily; urgency=low
+
+ * New wily upload with fix for go 1.4 syscall.Setgid() breakage
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 09 Jun 2015 10:02:04 +0200
+
+ubuntu-snappy (1.0.1-0ubuntu1) vivid; urgency=low
+
+ * fix symlink unpacking
+ * fix typo in apparmor rules generation
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 23 Apr 2015 16:09:56 +0200
+
+ubuntu-snappy (1.0-0ubuntu1) vivid; urgency=low
+
+ * 15.04 archive upload
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 23 Apr 2015 11:08:22 +0200
+
+ubuntu-snappy (0.1.2-0ubuntu1) vivid; urgency=medium
+
+ * initial ubuntu archive upload
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 13 Apr 2015 22:48:13 -0500
+
+ubuntu-snappy (0.1.1-0ubuntu1) vivid; urgency=low
+
+ * new snapshot
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 12 Feb 2015 13:51:22 +0100
+
+ubuntu-snappy (0.1-0ubuntu1) vivid; urgency=medium
+
+ * Initial packaging
+
+ -- Sergio Schvezov <sergio.schvezov@canonical.com> Fri, 06 Feb 2015 02:25:43 -0200
--- /dev/null
+Source: snapd
+Section: devel
+Priority: optional
+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+Build-Depends: autoconf,
+ automake,
+ autotools-dev,
+ bash-completion,
+ debhelper (>= 9),
+ dh-apparmor,
+ dh-autoreconf,
+ dh-golang (>=1.7),
+ dh-systemd,
+ fakeroot,
+ gettext,
+ gnupg2,
+ golang-any (>=2:1.6) | golang-1.6,
+ indent,
+ init-system-helpers,
+ libapparmor-dev,
+ libglib2.0-dev,
+ libseccomp-dev,
+ libudev-dev,
+ pkg-config,
+ python3,
+ python3-docutils,
+ python3-markdown,
+ squashfs-tools,
+ udev
+Standards-Version: 3.9.7
+Homepage: https://github.com/snapcore/snapd
+Vcs-Browser: https://github.com/snapcore/snapd
+Vcs-Git: https://github.com/snapcore/snapd.git
+
+Package: golang-github-ubuntu-core-snappy-dev
+Architecture: all
+Depends: golang-github-snapcore-snapd-dev, ${misc:Depends}
+Section: oldlibs
+Description: transitional dummy package
+ This is a transitional dummy package. It can safely be removed.
+
+Package: golang-github-snapcore-snapd-dev
+Architecture: all
+Breaks: golang-github-ubuntu-core-snappy-dev (<< 2.0.6),
+ golang-snappy-dev (<< 1.7.3+20160303ubuntu4)
+Replaces: golang-github-ubuntu-core-snappy-dev (<< 2.0.6),
+ golang-snappy-dev (<< 1.7.3+20160303ubuntu4)
+Depends: ${misc:Depends}
+Description: snappy development go packages.
+ Use these to use the snappy API.
+
+Package: snapd
+Architecture: any
+Depends: adduser,
+ apparmor (>= 2.10.95-0ubuntu2.2),
+ ca-certificates,
+ gnupg1 | gnupg,
+ snap-confine (= ${binary:Version}),
+ squashfs-tools,
+ systemd,
+ ubuntu-core-launcher (= ${binary:Version}),
+ ${misc:Depends},
+ ${shlibs:Depends}
+Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9)
+Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9)
+Conflicts: snap (<< 2013-11-29-1ubuntu1)
+Built-Using: ${misc:Built-Using}
+Description: Tool to interact with Ubuntu Core Snappy.
+ Install, configure, refresh and remove snap packages. Snaps are
+ 'universal' packages that work across many different Linux systems,
+ enabling secure distribution of the latest apps and utilities for
+ cloud, servers, desktops and the internet of things.
+ .
+ This is the CLI for snapd, a background service that takes care of
+ snaps on the system. Start with 'snap list' to see installed snaps.
+
+Package: ubuntu-snappy
+Architecture: all
+Depends: snapd, ${misc:Depends}
+Section: oldlibs
+Description: transitional dummy package
+ This is a transitional dummy package. It can safely be removed.
+
+Package: ubuntu-snappy-cli
+Architecture: all
+Depends: snapd, ${misc:Depends}
+Section: oldlibs
+Description: transitional dummy package
+ This is a transitional dummy package. It can safely be removed.
+
+Package: ubuntu-core-snapd-units
+Architecture: all
+Depends: snapd, ${misc:Depends}
+Section: oldlibs
+Description: transitional dummy package
+ This is a transitional dummy package. It can safely be removed.
+
+Package: snap-confine
+Architecture: any
+Breaks: ubuntu-core-launcher (<< 1.0.32)
+Replaces: ubuntu-core-launcher (<< 1.0.32)
+Depends: apparmor (>= 2.10.95-0ubuntu2.2), ${misc:Depends}, ${shlibs:Depends}
+Description: Support executable to apply confinement for snappy apps
+ This package contains an internal tool for applying confinement to snappy app.
+ The executable (snap-confine) is ran internally by snapd to apply confinement
+ to the started application process. The tool is written in C and carefully
+ reviewed to limit the attack surface on the security model of snapd.
+
+Package: ubuntu-core-launcher
+Architecture: any
+Depends: snap-confine (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Pre-Depends: dpkg (>= 1.15.7.2)
+Description: Launcher for ubuntu-core (snappy) apps
+ This package contains the launcher for launching snappy applications
--- /dev/null
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: snappy
+Source: https://github.com/snapcore/snapd
+
+Files: *
+Copyright: Copyright (C) 2014,2015 Canonical, Ltd.
+License: GPL-3
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the the GNU General Public License version 3, as
+ published by the Free Software Foundation.
+ .
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranties of
+ MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
+ PURPOSE. See the applicable version of the GNU Lesser General Public
+ License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the complete text of the GNU General Public License
+ can be found in `/usr/share/common-licenses/GPL-3'
--- /dev/null
+[DEFAULT]
+debian-branch = master
+export-dir = ../build-area
+postexport = govendor sync
--- /dev/null
+debian/tmp/usr/share/gocode/src/*
--- /dev/null
+debian/tmp/usr/bin/uboot-go
--- /dev/null
+#!/usr/bin/make -f
+# -*- makefile -*-
+
+#export DH_VERBOSE=1
+export DH_OPTIONS
+export DH_GOPKG := github.com/snapcore/snapd
+#export DEB_BUILD_OPTIONS=nocheck
+export DH_GOLANG_EXCLUDES=tests
+export DH_GOLANG_GO_GENERATE=1
+
+export PATH:=${PATH}:${CURDIR}
+# make sure that correct go version is found on trusty
+export PATH:=/usr/lib/go-1.6/bin:${PATH}
+
+include /etc/os-release
+
+SYSTEMD_UNITS_DESTDIR=
+ifeq (${VERSION_ID},"14.04")
+ # We are relying on a deputy systemd setup for trusty,
+ # in which systemd does not run as PID 1. To solve the
+ # problem of services shipping systemd units and upstart jobs
+ # being started twice, we altered systemd on trusty to ignore
+ # /lib/systemd/system and instead consider only selected units from
+ # /lib/systemd/upstart.
+ SYSTEMD_UNITS_DESTDIR="lib/systemd/upstart/"
+ # make sure that trusty's golang-1.6 is picked up correctly.
+ export PATH:=/usr/lib/go-1.6/bin:${PATH}
+else
+ SYSTEMD_UNITS_DESTDIR="lib/systemd/system/"
+endif
+
+# The go tool does not fully support vendoring with gccgo, but we can
+# work around that by constructing the appropriate -I flag by hand.
+GCCGO := $(shell go tool dist env > /dev/null 2>&1 && echo no || echo yes)
+
+BUILDFLAGS:=-buildmode=pie -pkgdir=$(CURDIR)/_build/std
+GCCGOFLAGS=
+ifeq ($(GCCGO),yes)
+GOARCH := $(shell go env GOARCH)
+GOOS := $(shell go env GOOS)
+BUILDFLAGS:=
+GCCGOFLAGS=-gccgoflags="-I $(CURDIR)/_build/pkg/gccgo_$(GOOS)_$(GOARCH)/$(DH_GOPKG)/vendor"
+export DH_GOLANG_GO_GENERATE=0
+endif
+
+# check if we need to include the testkeys in the binary
+TAGS=
+ifneq (,$(filter testkeys,$(DEB_BUILD_OPTIONS)))
+ TAGS=-tags withtestkeys
+endif
+
+# export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+# DPKG_EXPORT_BUILDFLAGS = 1
+# include /usr/share/dpkg/buildflags.mk
+
+# Currently, we enable confinement for Ubuntu only, not for derivatives,
+# because derivatives may have different kernels that don't support all the
+# required confinement features and we don't to mislead anyone about the
+# security of the system. Discuss a proper approach to this for downstreams
+# if and when they approach us
+ifeq ($(shell dpkg-vendor --query Vendor),Ubuntu)
+ VENDOR_ARGS=--enable-nvidia-ubuntu
+else
+ VENDOR_ARGS=--disable-apparmor
+endif
+
+%:
+ dh $@ --buildsystem=golang --with=golang --fail-missing --with systemd --builddirectory=_build
+
+override_dh_fixperms:
+ dh_fixperms -Xusr/lib/snapd/snap-confine
+
+override_dh_installdeb:
+ dh_apparmor --profile-name=usr.lib.snapd.snap-confine -psnap-confine
+ dh_installdeb
+
+override_dh_clean:
+ifneq (,$(TEST_GITHUB_AUTOPKGTEST))
+ # this will be set by the GITHUB webhook to trigger a autopkgtest
+ # we only need to run "govendor sync" here and then its ready
+ (export GOPATH="/tmp/go"; \
+ mkdir -p $$GOPATH/src/github.com/snapcore/; \
+ cp -ar . $$GOPATH/src/github.com/snapcore/snapd; \
+ go get -u github.com/kardianos/govendor; \
+ (cd $$GOPATH/src/github.com/snapcore/snapd ; $$GOPATH/bin/govendor sync); \
+ cp -ar $$GOPATH/src/github.com/snapcore/snapd/vendor/ .; \
+ )
+endif
+ dh_clean
+ # XXX: hacky
+ $(MAKE) -C cmd distclean || true
+
+override_dh_auto_build:
+ # usually done via `go generate` but that is not supported on powerpc
+ ./mkversion.sh
+ # Build golang bits
+ mkdir -p _build/src/$(DH_GOPKG)/cmd/snap/test-data
+ cp -a cmd/snap/test-data/*.gpg _build/src/$(DH_GOPKG)/cmd/snap/test-data/
+ dh_auto_build -- $(BUILDFLAGS) $(TAGS) $(GCCGOFLAGS)
+ # Build C bits, sadly manually
+ cd cmd && ( autoreconf -i -f )
+ cd cmd && ( ./configure --prefix=/usr --libexecdir=/usr/lib/snapd $(VENDOR_ARGS))
+ $(MAKE) -C cmd all
+
+override_dh_auto_test:
+ dh_auto_test -- $(GCCGOFLAGS)
+# a tested default (production) build should have no test keys
+ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+ # check that only the main trusted account-key is included
+ [ $$(strings _build/bin/snapd|grep -c -E "public-key-sha3-384: [a-zA-Z0-9_-]{64}") -eq 1 ]
+ strings _build/bin/snapd|grep -c "^public-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk$$"
+endif
+ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+ # run the snap-confine tests
+ $(MAKE) -C cmd check
+endif
+
+override_dh_systemd_enable:
+ # enable auto-import
+ dh_systemd_enable \
+ -psnapd \
+ snapd.autoimport.service
+ # we want the auto-update timer enabled by default
+ dh_systemd_enable \
+ -psnapd \
+ snapd.refresh.timer
+ # but the auto-update service disabled
+ dh_systemd_enable \
+ --no-enable \
+ -psnapd \
+ snapd.refresh.service
+ # enable snapd
+ dh_systemd_enable \
+ -psnapd \
+ snapd.socket
+ dh_systemd_enable \
+ -psnapd \
+ snapd.service
+
+override_dh_systemd_start:
+ # we want to start the auto-update timer
+ dh_systemd_start \
+ -psnapd \
+ snapd.refresh.timer
+ # but not start the service
+ dh_systemd_start \
+ --no-start \
+ -psnapd \
+ snapd.refresh.service
+ # start snapd
+ dh_systemd_start \
+ -psnapd \
+ snapd.socket
+ dh_systemd_start \
+ -psnapd \
+ snapd.service
+ # start autoimport
+ dh_systemd_start \
+ -psnapd \
+ snapd.autoimport.service
+
+override_dh_install:
+ # we do not need this in the package, its just needed during build
+ rm -rf ${CURDIR}/debian/tmp/usr/bin/xgettext-go
+ # uboot-go is not shippable
+ rm -f ${CURDIR}/debian/tmp/usr/bin/uboot-go
+ # toolbelt is not shippable
+ rm -f ${CURDIR}/debian/tmp/usr/bin/toolbelt
+ # we do not like /usr/bin/snappy anymore
+ rm -f ${CURDIR}/debian/tmp/usr/bin/snappy
+ # install dev package files
+ mkdir -p debian/golang-github-snapcore-snapd-dev/usr/share
+ rm -rf debian/tmp/usr/share/gocode/src/github.com/snapcore/snapd/cmd/snap-confine
+ cp -R debian/tmp/usr/share/gocode debian/golang-github-snapcore-snapd-dev/usr/share
+ # install udev stuff, must be installed before 80-udisks
+ install debian/snapd.autoimport.udev -D debian/snapd/lib/udev/rules.d/66-snapd-autoimport.rules
+
+ # install bash completion files
+ install --mode=0644 data/completion/snap -D debian/snapd/usr/share/bash-completion/completions/snap
+ # i18n stuff
+ mkdir -p debian/snapd/usr/share
+ if [ -d share/locale ]; then \
+ cp -R share/locale debian/snapd/usr/share; \
+ fi
+ # etc/profile.d contains the PATH extension for snap packages
+ mkdir -p debian/snapd/etc
+ cp -R etc/profile.d debian/snapd/etc
+ # etc/X11/Xsession.d will add to XDG_DATA_DIRS so that we have .desktop support
+ mkdir -p debian/snapd/etc
+ cp -R etc/X11 debian/snapd/etc
+ # we conditionally install snapd's systemd units
+ mkdir -p debian/snapd/$(SYSTEMD_UNITS_DESTDIR)
+ install --mode=0644 debian/snapd.refresh.timer debian/snapd/$(SYSTEMD_UNITS_DESTDIR)
+ install --mode=0644 debian/snapd.refresh.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)
+ install --mode=0644 debian/snapd.autoimport.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)
+ install --mode=0644 debian/*.socket debian/snapd/$(SYSTEMD_UNITS_DESTDIR)
+ install --mode=0644 debian/snapd.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)
+ifeq ($(RELEASE),trusty)
+ dh_link debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/snapd.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/multi-user.target.wants/snapd.service
+ dh_link debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/snapd.autoimport.service debian/snapd/$(SYSTEMD_UNITS_DESTDIR)/multi-user.target.wants/snapd.autoimport.service
+endif
+ $(MAKE) -C cmd install DESTDIR=$(CURDIR)/debian/tmp
+ dh_install
+
+override_dh_auto_install: snap.8
+ dh_auto_install -O--buildsystem=golang
+
+snap.8:
+ $(CURDIR)/_build/bin/snap help --man > $@
+
+override_dh_auto_clean:
+ dh_auto_clean -O--buildsystem=golang
+ rm -vf snap.8
--- /dev/null
+etc/apparmor.d/usr.lib.snapd.snap-confine
+lib/udev/rules.d/80-snappy-assign.rules
+lib/udev/snappy-app-dev
+usr/lib/snapd/snap-confine
+usr/lib/snapd/snap-discard-ns
+usr/share/man/man5/snap-confine.5
+usr/share/man/man5/snap-discard-ns.5
--- /dev/null
+[Unit]
+Description=Auto import assertions from block devices
+After=snapd.service snapd.socket
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/snap auto-import
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+# probe for assertions, must run before udisks2
+ACTION=="add", SUBSYSTEM=="block" \
+ RUN+="/usr/bin/unshare -m /usr/bin/snap auto-import --mount=/dev/%k"
--- /dev/null
+snap
+usr/lib/snapd
+var/lib/snapd/auto-import
+var/lib/snapd/desktop
+var/lib/snapd/environment
+var/lib/snapd/firstboot
+var/lib/snapd/lib/gl
+var/lib/snapd/snaps/partial
+var/lib/snapd/void
+var/snap
--- /dev/null
+usr/bin/snap
+usr/bin/snap-exec /usr/lib/snapd/
+usr/bin/snapctl
+usr/bin/snapd /usr/lib/snapd/
+
+data/info /usr/lib/snapd/
--- /dev/null
+# keep mount point busy
+# we used to ship a custom grub config that is no longer needed
+rm_conffile /etc/grub.d/09_snappy 1.7.3ubuntu1
+rm_conffile /etc/ld.so.conf.d/snappy.conf 2.0.7~
--- /dev/null
+#!/bin/sh
+
+set -e
+
+#DEBHELPER#
+
+
+case "$1" in
+ configure)
+ # ensure /var/lib/snapd/lib/gl is cleared
+ if dpkg --compare-versions "$2" lt-nl "2.0.7"; then
+ ldconfig
+ fi
+esac
--- /dev/null
+#!/bin/sh
+
+set -e
+
+systemctl_stop() {
+ unit="$1"
+ if systemctl is-active -q "$unit"; then
+ echo "Stoping $unit"
+ systemctl stop -q "$unit" || true
+ fi
+}
+
+if [ "$1" = "purge" ]; then
+ mounts=$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')
+ services=$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ')
+ for unit in $services $mounts; do
+ # ensure its really a snapp mount unit or systemd unit
+ if ! grep -q 'What=/var/lib/snapd/snaps/' "/etc/systemd/system/$unit" && ! grep -q 'X-Snappy=yes' "/etc/systemd/system/$unit"; then
+ echo "Skipping non-snapd systemd unit $unit"
+ continue
+ fi
+
+ echo "Stopping $unit"
+ systemctl_stop "$unit"
+
+ # if it is a mount unit, we can find the snap name in the mount
+ # unit (we just ignore unit files)
+ snap=$(grep 'Where=/snap/' "/etc/systemd/system/$unit"|cut -f3 -d/)
+ rev=$(grep 'Where=/snap/' "/etc/systemd/system/$unit"|cut -f4 -d/)
+ if [ -n "$snap" ]; then
+ echo "Removing snap $snap"
+ # generated binaries
+ rm -f "/snap/bin/$snap"
+ rm -f "/snap/bin/$snap".*
+ # snap mount dir
+ umount -l "/snap/$snap/$rev" 2> /dev/null || true
+ rm -rf "/snap/$snap/$rev"
+ rm -f "/snap/$snap/current"
+ # snap data dir
+ rm -rf "/var/snap/$snap/$rev"
+ rm -rf "/var/snap/$snap/common"
+ rm -f "/var/snap/$snap/current"
+ # opportunistic remove (may fail if there are still revisions left
+ for d in "/snap/bin" "/snap/$snap" "/var/snap/$snap" "/snap" "/var/snap"; do
+ if [ -d "$d" ]; then
+ rmdir --ignore-fail-on-non-empty $d
+ fi
+ done
+ fi
+
+ echo "Removing $unit"
+ rm -f "/etc/systemd/system/$unit"
+ rm -f "/etc/systemd/system/multi-user.target.wants/$unit"
+ done
+
+ echo "Discarding preserved snap namespaces"
+ # opportunistic as those might not be actually mounted
+ for mnt in /run/snapd/ns/*.mnt; do
+ umount -l "$mnt" || true
+ done
+ umount -l /run/snapd/ns/ || true
+
+ echo "Removing snapd state"
+ rm -rf /var/lib/snapd
+fi
+
+#DEBHELPER#
--- /dev/null
+[Unit]
+Description=Automatically refresh installed snaps
+After=network-online.target snapd.socket
+Requires=snapd.socket
+ConditionPathExistsGlob=/snap/*/current
+Documentation=man:snap(1)
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/snap refresh
+Environment=SNAP_REFRESH_FROM_TIMER=1
--- /dev/null
+[Unit]
+Description=Timer to automatically refresh installed snaps
+
+[Timer]
+# spread the requests gently
+# https://bugs.launchpad.net/snappy/+bug/1537793
+OnCalendar=23,05,11,17:00
+RandomizedDelaySec=6h
+AccuracySec=10min
+Persistent=true
+OnStartupSec=15m
+
+[Install]
+WantedBy=timers.target
--- /dev/null
+[Unit]
+Description=Snappy daemon
+Requires=snapd.socket
+
+[Service]
+ExecStart=/usr/lib/snapd/snapd
+EnvironmentFile=/etc/environment
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+[Unit]
+Description=Socket activation for snappy daemon
+
+[Socket]
+ListenStream=/run/snapd.socket
+ListenStream=/run/snapd-snap.socket
+SocketMode=0666
+# these are the defaults, but can't hurt to specify them anyway:
+SocketUser=root
+SocketGroup=root
+
+[Install]
+WantedBy=sockets.target
--- /dev/null
+3.0 (native)
--- /dev/null
+## Autopkgtest
+
+In order to run the autopkgtest suite locally you need first to generate an image:
+
+ $ adt-buildvm-ubuntu-cloud -a amd64 -r xenial -v
+
+This will create a `adt-xenial-amd64-cloud.img` file, then you can run the tests from
+the project's root with:
+
+ $ adt-run --unbuilt-tree . --- qemu ./adt-xenial-amd64-cloud.img
--- /dev/null
+Tests: integrationtests
+Restrictions: allow-stderr, isolation-container, rw-build-tree, needs-root, breaks-testbed
+Depends: @builddeps@,
+ bzr,
+ ca-certificates,
+ git,
+ golang-golang-x-net-dev,
+ openssh-server,
+ snapd,
+ unity,
+ x11-utils,
+ xvfb
--- /dev/null
+#!/bin/sh
+
+set -ex
+
+# required for the debian adt host
+mkdir -p /etc/systemd/system/snapd.service.d/
+if [ "$http_proxy" != "" ]; then
+ cat <<EOF | tee /etc/systemd/system/snapd.service.d/proxy.conf
+[Service]
+Environment=http_proxy=$http_proxy
+Environment=https_proxy=$http_proxy
+EOF
+
+ # ensure environment is updated
+ echo "http_proxy=$http_proxy" >> /etc/environment
+ echo "https_proxy=$http_proxy" >> /etc/environment
+fi
+systemctl daemon-reload
+
+# ensure we can do a connect to localhost
+echo ubuntu:ubuntu|chpasswd
+sed -i 's/\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config
+systemctl reload sshd.service
+
+# and now run spread against localhost
+. /etc/os-release
+export GOPATH=/tmp/go
+go get -u github.com/snapcore/spread/cmd/spread
+/tmp/go/bin/spread -v autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture)
--- /dev/null
+{
+ "FromBranch": false
+}
--- /dev/null
+usr/bin/ubuntu-core-launcher
+usr/share/man/man1/ubuntu-core-launcher.1
--- /dev/null
+usr/lib/snapd
+var/lib/snapd/snaps
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package dirs
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// the various file paths
+var (
+ GlobalRootDir string
+
+ SnapMountDir string
+ SnapBlobDir string
+ SnapDataDir string
+ SnapDataHomeGlob string
+ SnapAppArmorDir string
+ AppArmorCacheDir string
+ SnapAppArmorAdditionalDir string
+ SnapSeccompDir string
+ SnapMountPolicyDir string
+ SnapUdevRulesDir string
+ SnapKModModulesDir string
+ LocaleDir string
+ SnapMetaDir string
+ SnapdSocket string
+ SnapSocket string
+ SnapRunNsDir string
+
+ SnapSeedDir string
+ SnapDeviceDir string
+
+ SnapAssertsDBDir string
+ SnapTrustedAccountKey string
+ SnapAssertsSpoolDir string
+
+ SnapStateFile string
+
+ SnapBinariesDir string
+ SnapServicesDir string
+ SnapDesktopFilesDir string
+ SnapBusPolicyDir string
+
+ CloudMetaDataFile string
+
+ ClassicDir string
+
+ LibExecDir string
+
+ XdgRuntimeDirGlob string
+)
+
+var (
+ // not exported because it does not honor the global rootdir
+ snappyDir = filepath.Join("var", "lib", "snapd")
+)
+
+func init() {
+ // init the global directories at startup
+ root := os.Getenv("SNAPPY_GLOBAL_ROOT")
+
+ SetRootDir(root)
+}
+
+// StripRootDir strips the custom global root directory from the specified argument.
+func StripRootDir(dir string) string {
+ if !filepath.IsAbs(dir) {
+ panic(fmt.Sprintf("supplied path is not absolute %q", dir))
+ }
+ if !strings.HasPrefix(dir, GlobalRootDir) {
+ panic(fmt.Sprintf("supplied path is not related to global root %q", dir))
+ }
+ result, err := filepath.Rel(GlobalRootDir, dir)
+ if err != nil {
+ panic(err)
+ }
+ return "/" + result
+}
+
+// SetRootDir allows settings a new global root directory, this is useful
+// for e.g. chroot operations
+func SetRootDir(rootdir string) {
+ if rootdir == "" {
+ rootdir = "/"
+ }
+ GlobalRootDir = rootdir
+
+ SnapMountDir = filepath.Join(rootdir, "/snap")
+ SnapDataDir = filepath.Join(rootdir, "/var/snap")
+ SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/snap/")
+ SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles")
+ AppArmorCacheDir = filepath.Join(rootdir, "/var/cache/apparmor")
+ SnapAppArmorAdditionalDir = filepath.Join(rootdir, snappyDir, "apparmor", "additional")
+ SnapSeccompDir = filepath.Join(rootdir, snappyDir, "seccomp", "profiles")
+ SnapMountPolicyDir = filepath.Join(rootdir, snappyDir, "mount")
+ SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta")
+ SnapBlobDir = filepath.Join(rootdir, snappyDir, "snaps")
+ SnapDesktopFilesDir = filepath.Join(rootdir, snappyDir, "desktop", "applications")
+ SnapRunNsDir = filepath.Join(rootdir, "/run/snapd/ns")
+
+ // keep in sync with the debian/snapd.socket file:
+ SnapdSocket = filepath.Join(rootdir, "/run/snapd.socket")
+ SnapSocket = filepath.Join(rootdir, "/run/snapd-snap.socket")
+
+ SnapAssertsDBDir = filepath.Join(rootdir, snappyDir, "assertions")
+ SnapAssertsSpoolDir = filepath.Join(rootdir, "run/snapd/auto-import")
+
+ SnapStateFile = filepath.Join(rootdir, snappyDir, "state.json")
+
+ SnapSeedDir = filepath.Join(rootdir, snappyDir, "seed")
+ SnapDeviceDir = filepath.Join(rootdir, snappyDir, "device")
+
+ SnapBinariesDir = filepath.Join(SnapMountDir, "bin")
+ SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system")
+ SnapBusPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d")
+
+ CloudMetaDataFile = filepath.Join(rootdir, "/var/lib/cloud/seed/nocloud-net/meta-data")
+
+ SnapUdevRulesDir = filepath.Join(rootdir, "/etc/udev/rules.d")
+
+ SnapKModModulesDir = filepath.Join(rootdir, "/etc/modules-load.d/")
+
+ LocaleDir = filepath.Join(rootdir, "/usr/share/locale")
+ ClassicDir = filepath.Join(rootdir, "/writable/classic")
+
+ LibExecDir = filepath.Join(rootdir, "/usr/lib/snapd")
+
+ XdgRuntimeDirGlob = filepath.Join(rootdir, "/run/user/*/")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package dirs_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&DirsTestSuite{})
+
+type DirsTestSuite struct{}
+
+func (s *DirsTestSuite) TestStripRootDir(c *C) {
+ // strip does nothing if the default (empty) root directory is used
+ c.Check(dirs.StripRootDir("/foo/bar"), Equals, "/foo/bar")
+ // strip only works on absolute paths
+ c.Check(func() { dirs.StripRootDir("relative") }, Panics, `supplied path is not absolute "relative"`)
+ // with an alternate root
+ dirs.SetRootDir("/alt/")
+ defer dirs.SetRootDir("")
+ // strip behaves as expected, returning absolute paths without the prefix
+ c.Check(dirs.StripRootDir("/alt/foo/bar"), Equals, "/foo/bar")
+ // strip only works on paths that begin with the global root directory
+ c.Check(func() { dirs.StripRootDir("/other/foo/bar") }, Panics, `supplied path is not related to global root "/other/foo/bar"`)
+}
--- /dev/null
+### Moved to https://github.com/snapcore/snapd/wiki
--- /dev/null
+# This file is sourced by Xsession(5), not executed.
+# Add the additional snappy desktop path
+
+if [ -z "$XDG_DATA_DIRS" ]; then
+ # 60x11-common_xdg_path does not always set XDG_DATA_DIRS
+ # so we ensure we have sensible defaults here (LP: #1575014)
+ # as a workaround
+ XDG_DATA_DIRS=/usr/local/share/:/usr/share/:/var/lib/snapd/desktop
+else
+ XDG_DATA_DIRS="$XDG_DATA_DIRS":/var/lib/snapd/desktop
+fi
+export XDG_DATA_DIRS
--- /dev/null
+# Expand the $PATH to include /snap/bin which is what snappy applications
+# use
+PATH=$PATH:/snap/bin
--- /dev/null
+#!/bin/sh
+
+set -e
+
+./run-checks --unit
+
+go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html
+
+echo "Coverage html reports are available in .coverage/coverage.html"
--- /dev/null
+#!/bin/sh
+
+set -e
+
+if [ ! -d .git ]; then
+ echo "not running in a git checkout, skipping"
+ exit 0
+fi
+
+ID="$1"
+VERSION_ID="$2"
+
+DST="debian-$ID-$VERSION_ID"
+rm -rf $DST
+mkdir -p "$DST"
+
+if ! git remote | grep upstream; then
+ git remote add upstream https://github.com/snapcore/snapd
+fi
+git fetch upstream
+
+git ls-tree upstream/"$ID"/"$VERSION_ID" debian/ | while read line ; do
+ file=$(basename $(echo $line | cut -d " " -f4))
+ hash=$(echo $line | cut -d " " -f3)
+ type=$(echo $line | cut -d " " -f2)
+ # FIXME: deal with subdirs
+ if [ "$type" = "blob" ]; then
+ git cat-file -p $hash > "$DST/$file"
+ fi
+done
--- /dev/null
+#!/bin/sh
+
+set -eu
+
+if [ -z "$(which govendor)" ];then
+ echo Installing govendor
+ go get -u github.com/kardianos/govendor
+fi
+export PATH=$PATH:$GOPATH/bin
+
+echo Obtaining dependencies
+govendor sync
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package i18n // import "github.com/snapcore/snapd/i18n/dumb"
+
+func G(s string) string {
+ return s
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package i18n
+
+//go:generate update-pot
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ /* this is actually "github.com/ojii/gettext.go", however because
+ https://github.com/ojii/gettext.go/pull/4
+ is not merged as of this writtting we want to use this fork
+ */
+ "github.com/mvo5/gettext.go"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+// TEXTDOMAIN is the message domain used by snappy; see dgettext(3)
+// for more information.
+var (
+ TEXTDOMAIN = "snappy"
+ locale gettext.Catalog
+ translations gettext.Translations
+)
+
+func init() {
+ bindTextDomain(TEXTDOMAIN, "/usr/share/locale")
+ setLocale("")
+}
+
+func langpackResolver(root string, locale string, domain string) string {
+
+ // first check for the real locale (e.g. de_DE)
+ // then try to simplify the locale (e.g. de_DE -> de)
+ locales := []string{locale, strings.SplitN(locale, "_", 2)[0]}
+ for _, locale := range locales {
+ r := filepath.Join(locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain))
+
+ // ubuntu uses /usr/lib/locale-langpack and patches the glibc gettext
+ // implementation
+ langpack := filepath.Join(root, "..", "locale-langpack", r)
+ if osutil.FileExists(langpack) {
+ return langpack
+ }
+
+ regular := filepath.Join(root, r)
+ if osutil.FileExists(regular) {
+ return regular
+ }
+ }
+
+ return ""
+}
+
+func bindTextDomain(domain, dir string) {
+ translations = gettext.NewTranslations(dir, domain, langpackResolver)
+}
+
+func setLocale(loc string) {
+ if loc == "" {
+ loc = os.Getenv("LC_MESSAGES")
+ if loc == "" {
+ loc = os.Getenv("LANG")
+ }
+ }
+ // de_DE.UTF-8, de_DE@euro all need to get simplified
+ loc = strings.Split(loc, "@")[0]
+ loc = strings.Split(loc, ".")[0]
+
+ locale = translations.Locale(loc)
+}
+
+// G is the shorthand for Gettext
+func G(msgid string) string {
+ return locale.Gettext(msgid)
+}
+
+// NG is the shorthand for NGettext
+func NG(msgid string, msgidPlural string, n uint32) string {
+ return locale.NGettext(msgid, msgidPlural, n)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package i18n
+
+import (
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+var mockLocalePo = []byte(`
+msgid ""
+msgstr ""
+"Project-Id-Version: snappy\n"
+"Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
+"POT-Creation-Date: 2015-06-16 09:08+0200\n"
+"Language: en_DK\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;>\n"
+
+msgid "plural_1"
+msgid_plural "plural_2"
+msgstr[0] "translated plural_1"
+msgstr[1] "translated plural_2"
+
+msgid "singular"
+msgstr "translated singular"
+`)
+
+func makeMockTranslations(c *C, localeDir string) {
+ fullLocaleDir := filepath.Join(localeDir, "en_DK", "LC_MESSAGES")
+ err := os.MkdirAll(fullLocaleDir, 0755)
+ c.Assert(err, IsNil)
+
+ po := filepath.Join(fullLocaleDir, "snappy-test.po")
+ mo := filepath.Join(fullLocaleDir, "snappy-test.mo")
+ err = ioutil.WriteFile(po, mockLocalePo, 0644)
+ c.Assert(err, IsNil)
+
+ cmd := exec.Command("msgfmt", po, "--output-file", mo)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ c.Assert(err, IsNil)
+}
+
+type i18nTestSuite struct {
+ origLang string
+ origLcMessages string
+}
+
+var _ = Suite(&i18nTestSuite{})
+
+func (s *i18nTestSuite) SetUpTest(c *C) {
+ // this dir contains a special hand-crafted en_DK/snappy-test.mo
+ // file
+ localeDir := c.MkDir()
+ makeMockTranslations(c, localeDir)
+
+ // we use a custom test mo file
+ TEXTDOMAIN = "snappy-test"
+
+ s.origLang = os.Getenv("LANG")
+ s.origLcMessages = os.Getenv("LC_MESSAGES")
+
+ bindTextDomain("snappy-test", localeDir)
+ os.Setenv("LANG", "en_DK.UTF-8")
+ setLocale("")
+}
+
+func (s *i18nTestSuite) TearDownTest(c *C) {
+ os.Setenv("LANG", s.origLang)
+ os.Setenv("LC_MESSAGES", s.origLcMessages)
+}
+
+func (s *i18nTestSuite) TestTranslatedSingular(c *C) {
+ // no G() to avoid adding the test string to snappy-pot
+ var Gtest = G
+ c.Assert(Gtest("singular"), Equals, "translated singular")
+}
+
+func (s *i18nTestSuite) TestTranslatesPlural(c *C) {
+ // no NG() to avoid adding the test string to snappy-pot
+ var NGtest = NG
+ c.Assert(NGtest("plural_1", "plural_2", 1), Equals, "translated plural_1")
+}
+
+func (s *i18nTestSuite) TestTranslatedMissingLangNoCrash(c *C) {
+ setLocale("invalid")
+
+ // no G() to avoid adding the test string to snappy-pot
+ var Gtest = G
+ c.Assert(Gtest("singular"), Equals, "singular")
+}
+
+func (s *i18nTestSuite) TestInvalidTextDomainDir(c *C) {
+ bindTextDomain("snappy-test", "/random/not/existing/dir")
+ setLocale("invalid")
+
+ // no G() to avoid adding the test string to snappy-pot
+ var Gtest = G
+ c.Assert(Gtest("singular"), Equals, "singular")
+}
--- /dev/null
+package main
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type msgID struct {
+ msgidPlural string
+ comment string
+ fname string
+ line int
+ formatHint string
+}
+
+var msgIDs map[string][]msgID
+
+func formatComment(com string) string {
+ out := ""
+ for _, rawline := range strings.Split(com, "\n") {
+ line := rawline
+ line = strings.TrimPrefix(line, "//")
+ line = strings.TrimPrefix(line, "/*")
+ line = strings.TrimSuffix(line, "*/")
+ line = strings.TrimSpace(line)
+ if line != "" {
+ out += fmt.Sprintf("#. %s\n", line)
+ }
+ }
+
+ return out
+}
+
+func findCommentsForTranslation(fset *token.FileSet, f *ast.File, posCall token.Position) string {
+ com := ""
+ for _, cg := range f.Comments {
+ // search for all comments in the previous line
+ for i := len(cg.List) - 1; i >= 0; i-- {
+ c := cg.List[i]
+
+ posComment := fset.Position(c.End())
+ //println(posCall.Line, posComment.Line, c.Text)
+ if posCall.Line == posComment.Line+1 {
+ posCall = posComment
+ com = fmt.Sprintf("%s\n%s", c.Text, com)
+ }
+ }
+ }
+
+ // only return if we have a matching prefix
+ formatedComment := formatComment(com)
+ needle := fmt.Sprintf("#. %s", opts.AddCommentsTag)
+ if !strings.HasPrefix(formatedComment, needle) {
+ formatedComment = ""
+ }
+
+ return formatedComment
+}
+
+func constructValue(val interface{}) string {
+ switch val.(type) {
+ case *ast.BasicLit:
+ return val.(*ast.BasicLit).Value
+ // this happens for constructs like:
+ // gettext.Gettext("foo" + "bar")
+ case *ast.BinaryExpr:
+ // we only support string concat
+ if val.(*ast.BinaryExpr).Op != token.ADD {
+ return ""
+ }
+ left := constructValue(val.(*ast.BinaryExpr).X)
+ // strip right " (or `)
+ left = left[0 : len(left)-1]
+ right := constructValue(val.(*ast.BinaryExpr).Y)
+ // strip left " (or `)
+ right = right[1:]
+ return left + right
+ default:
+ panic(fmt.Sprintf("unknown type: %v", val))
+ }
+}
+
+func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bool {
+ // FIXME: this assume we always have a "gettext.Gettext" style keyword
+ l := strings.Split(opts.Keyword, ".")
+ gettextSelector := l[0]
+ gettextFuncName := l[1]
+
+ l = strings.Split(opts.KeywordPlural, ".")
+ gettextSelectorPlural := l[0]
+ gettextFuncNamePlural := l[1]
+
+ switch x := n.(type) {
+ case *ast.CallExpr:
+ if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
+ i18nStr := ""
+ i18nStrPlural := ""
+ if sel.Sel.Name == gettextFuncNamePlural && sel.X.(*ast.Ident).Name == gettextSelectorPlural {
+ i18nStr = x.Args[0].(*ast.BasicLit).Value
+ i18nStrPlural = x.Args[1].(*ast.BasicLit).Value
+ }
+
+ if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector {
+ i18nStr = constructValue(x.Args[0])
+ }
+
+ formatI18nStr := func(s string) string {
+ if s == "" {
+ return ""
+ }
+ // the "`" is special
+ if s[0] == '`' {
+ // replace inner " with \"
+ s = strings.Replace(s, "\"", "\\\"", -1)
+ // replace \n with \\n
+ s = strings.Replace(s, "\n", "\\n", -1)
+ }
+ // strip leading and trailing " (or `)
+ s = s[1 : len(s)-1]
+ return s
+ }
+
+ // FIXME: too simplistic(?), no %% is considered
+ formatHint := ""
+ if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") {
+ // well, not quite correct but close enough
+ formatHint = "c-format"
+ }
+
+ if i18nStr != "" {
+ msgidStr := formatI18nStr(i18nStr)
+ posCall := fset.Position(n.Pos())
+ msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{
+ formatHint: formatHint,
+ msgidPlural: formatI18nStr(i18nStrPlural),
+ fname: posCall.Filename,
+ line: posCall.Line,
+ comment: findCommentsForTranslation(fset, f, posCall),
+ })
+ }
+ }
+ }
+
+ return true
+}
+
+func processFiles(args []string) error {
+ // go over the input files
+ msgIDs = make(map[string][]msgID)
+
+ fset := token.NewFileSet()
+ for _, fname := range args {
+ if err := processSingleGoSource(fset, fname); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func processSingleGoSource(fset *token.FileSet, fname string) error {
+ fnameContent, err := ioutil.ReadFile(fname)
+ if err != nil {
+ panic(err)
+ }
+
+ // Create the AST by parsing src.
+ f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments)
+ if err != nil {
+ panic(err)
+ }
+
+ ast.Inspect(f, func(n ast.Node) bool {
+ return inspectNodeForTranslations(fset, f, n)
+ })
+
+ return nil
+}
+
+var formatTime = func() string {
+ return time.Now().Format("2006-01-02 15:04-0700")
+}
+
+func writePotFile(out io.Writer) {
+
+ header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr "Project-Id-Version: %s\n"
+ "Report-Msgid-Bugs-To: %s\n"
+ "POT-Creation-Date: %s\n"
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+ "Language-Team: LANGUAGE <LL@li.org>\n"
+ "Language: \n"
+ "MIME-Version: 1.0\n"
+ "Content-Type: text/plain; charset=CHARSET\n"
+ "Content-Transfer-Encoding: 8bit\n"
+
+`, opts.PackageName, opts.MsgIDBugsAddress, formatTime())
+ fmt.Fprintf(out, "%s", header)
+
+ // yes, this is the way to do it in go
+ sortedKeys := []string{}
+ for k := range msgIDs {
+ sortedKeys = append(sortedKeys, k)
+ }
+ if opts.SortOutput {
+ sort.Strings(sortedKeys)
+ }
+
+ // FIXME: use template here?
+ for _, k := range sortedKeys {
+ msgidList := msgIDs[k]
+ for _, msgid := range msgidList {
+ if opts.AddComments || opts.AddCommentsTag != "" {
+ fmt.Fprintf(out, "%s", msgid.comment)
+ }
+ }
+ if !opts.NoLocation {
+ fmt.Fprintf(out, "#:")
+ for _, msgid := range msgidList {
+ fmt.Fprintf(out, " %s:%d", msgid.fname, msgid.line)
+ }
+ fmt.Fprintf(out, "\n")
+ }
+ msgid := msgidList[0]
+ if msgid.formatHint != "" {
+ fmt.Fprintf(out, "#, %s\n", msgid.formatHint)
+ }
+ var formatOutput = func(in string) string {
+ // split string with \n into multiple lines
+ // to make the output nicer
+ out := strings.Replace(in, "\\n", "\\n\"\n \"", -1)
+ // cleanup too aggressive splitting (empty "" lines)
+ return strings.TrimSuffix(out, "\"\n \"")
+ }
+ fmt.Fprintf(out, "msgid \"%v\"\n", formatOutput(k))
+ if msgid.msgidPlural != "" {
+ fmt.Fprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural))
+ fmt.Fprintf(out, "msgstr[0] \"\"\n")
+ fmt.Fprintf(out, "msgstr[1] \"\"\n")
+ } else {
+ fmt.Fprintf(out, "msgstr \"\"\n")
+ }
+ fmt.Fprintf(out, "\n")
+ }
+
+}
+
+// FIXME: this must be setable via go-flags
+var opts struct {
+ Output string `short:"o" long:"output" description:"output to specified file"`
+
+ AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"`
+
+ AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"`
+
+ SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"`
+
+ NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"`
+
+ MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"`
+
+ PackageName string `long:"package-name" description:"set package name in output"`
+
+ Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"`
+ KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"`
+}
+
+func main() {
+ // parse args
+ args, err := flags.ParseArgs(&opts, os.Args)
+ if err != nil {
+ log.Fatalf("ParseArgs failed %s", err)
+ }
+
+ if err := processFiles(args[1:]); err != nil {
+ log.Fatalf("processFiles failed with: %s", err)
+ }
+
+ out := os.Stdout
+ if opts.Output != "" {
+ var err error
+ out, err = os.Create(opts.Output)
+ if err != nil {
+ log.Fatalf("failed to create %s: %s", opts.Output, err)
+ }
+ }
+ writePotFile(out)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type xgettextTestSuite struct {
+}
+
+var _ = Suite(&xgettextTestSuite{})
+
+// test helper
+func makeGoSourceFile(c *C, content []byte) string {
+ fname := filepath.Join(c.MkDir(), "foo.go")
+ err := ioutil.WriteFile(fname, []byte(content), 0644)
+ c.Assert(err, IsNil)
+
+ return fname
+}
+
+func (s *xgettextTestSuite) SetUpTest(c *C) {
+ // our test defaults
+ opts.NoLocation = false
+ opts.AddCommentsTag = "TRANSLATORS:"
+ opts.Keyword = "i18n.G"
+ opts.KeywordPlural = "i18n.NG"
+ opts.SortOutput = true
+ opts.PackageName = "snappy"
+ opts.MsgIDBugsAddress = "snappy-devel@lists.ubuntu.com"
+
+ // mock time
+ formatTime = func() string {
+ return "2015-06-30 14:48+0200"
+ }
+}
+
+func (s *xgettextTestSuite) TestFormatComment(c *C) {
+ var tests = []struct {
+ in string
+ out string
+ }{
+ {in: "// foo ", out: "#. foo\n"},
+ {in: "/* foo */", out: "#. foo\n"},
+ {in: "/* foo\n */", out: "#. foo\n"},
+ {in: "/* foo\nbar */", out: "#. foo\n#. bar\n"},
+ }
+
+ for _, test := range tests {
+ c.Assert(formatComment(test.in), Equals, test.out)
+ }
+}
+
+func (s *xgettextTestSuite) TestProcessFilesSimple(c *C) {
+ fname := makeGoSourceFile(c, []byte(`package main
+
+func main() {
+ // TRANSLATORS: foo comment
+ i18n.G("foo")
+}
+`))
+ err := processFiles([]string{fname})
+ c.Assert(err, IsNil)
+
+ c.Assert(msgIDs, DeepEquals, map[string][]msgID{
+ "foo": {
+ {
+ comment: "#. TRANSLATORS: foo comment\n",
+ fname: fname,
+ line: 5,
+ },
+ },
+ })
+}
+
+func (s *xgettextTestSuite) TestProcessFilesMultiple(c *C) {
+ fname := makeGoSourceFile(c, []byte(`package main
+
+func main() {
+ // TRANSLATORS: foo comment
+ i18n.G("foo")
+
+ // TRANSLATORS: bar comment
+ i18n.G("foo")
+}
+`))
+ err := processFiles([]string{fname})
+ c.Assert(err, IsNil)
+
+ c.Assert(msgIDs, DeepEquals, map[string][]msgID{
+ "foo": {
+ {
+ comment: "#. TRANSLATORS: foo comment\n",
+ fname: fname,
+ line: 5,
+ },
+ {
+ comment: "#. TRANSLATORS: bar comment\n",
+ fname: fname,
+ line: 8,
+ },
+ },
+ })
+}
+
+const header = `# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr "Project-Id-Version: snappy\n"
+ "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
+ "POT-Creation-Date: 2015-06-30 14:48+0200\n"
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+ "Language-Team: LANGUAGE <LL@li.org>\n"
+ "Language: \n"
+ "MIME-Version: 1.0\n"
+ "Content-Type: text/plain; charset=CHARSET\n"
+ "Content-Transfer-Encoding: 8bit\n"
+`
+
+func (s *xgettextTestSuite) TestWriteOutputSimple(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo": {
+ {
+ fname: "fname",
+ line: 2,
+ comment: "#. foo\n",
+ },
+ },
+ }
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#. foo
+#: fname:2
+msgid "foo"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputMultiple(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo": {
+ {
+ fname: "fname",
+ line: 2,
+ comment: "#. comment1\n",
+ },
+ {
+ fname: "fname",
+ line: 4,
+ comment: "#. comment2\n",
+ },
+ },
+ }
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#. comment1
+#. comment2
+#: fname:2 fname:4
+msgid "foo"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputNoComment(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo": {
+ {
+ fname: "fname",
+ line: 2,
+ },
+ },
+ }
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#: fname:2
+msgid "foo"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputNoLocation(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo": {
+ {
+ fname: "fname",
+ line: 2,
+ },
+ },
+ }
+
+ opts.NoLocation = true
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+msgid "foo"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputFormatHint(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo": {
+ {
+ fname: "fname",
+ line: 2,
+ formatHint: "c-format",
+ },
+ },
+ }
+
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#: fname:2
+#, c-format
+msgid "foo"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputPlural(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo": {
+ {
+ msgidPlural: "plural",
+ fname: "fname",
+ line: 2,
+ },
+ },
+ }
+
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#: fname:2
+msgid "foo"
+msgid_plural "plural"
+msgstr[0] ""
+msgstr[1] ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputSorted(c *C) {
+ msgIDs = map[string][]msgID{
+ "aaa": {
+ {
+ fname: "fname",
+ line: 2,
+ },
+ },
+ "zzz": {
+ {
+ fname: "fname",
+ line: 2,
+ },
+ },
+ }
+
+ opts.SortOutput = true
+ // we need to run this a bunch of times as the ordering might
+ // be right by pure chance
+ for i := 0; i < 10; i++ {
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#: fname:2
+msgid "aaa"
+msgstr ""
+
+#: fname:2
+msgid "zzz"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+ }
+}
+
+func (s *xgettextTestSuite) TestIntegration(c *C) {
+ fname := makeGoSourceFile(c, []byte(`package main
+
+func main() {
+ // TRANSLATORS: foo comment
+ // with multiple lines
+ i18n.G("foo")
+
+ // this comment has no translators tag
+ i18n.G("abc")
+
+ // TRANSLATORS: plural
+ i18n.NG("singular", "plural", 99)
+
+ i18n.G("zz %s")
+}
+`))
+
+ // a real integration test :)
+ outName := filepath.Join(c.MkDir(), "snappy.pot")
+ os.Args = []string{"test-binary",
+ "--output", outName,
+ "--keyword", "i18n.G",
+ "--keyword-plural", "i18n.NG",
+ "--msgid-bugs-address", "snappy-devel@lists.ubuntu.com",
+ "--package-name", "snappy",
+ fname,
+ }
+ main()
+
+ // verify its what we expect
+ got, err := ioutil.ReadFile(outName)
+ c.Assert(err, IsNil)
+ expected := fmt.Sprintf(`%s
+#: %[2]s:9
+msgid "abc"
+msgstr ""
+
+#. TRANSLATORS: foo comment
+#. with multiple lines
+#: %[2]s:6
+msgid "foo"
+msgstr ""
+
+#. TRANSLATORS: plural
+#: %[2]s:12
+msgid "singular"
+msgid_plural "plural"
+msgstr[0] ""
+msgstr[1] ""
+
+#: %[2]s:14
+#, c-format
+msgid "zz %%s"
+msgstr ""
+
+`, header, fname)
+ c.Assert(string(got), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestProcessFilesConcat(c *C) {
+ fname := makeGoSourceFile(c, []byte(`package main
+
+func main() {
+ // TRANSLATORS: foo comment
+ i18n.G("foo\n" + "bar\n" + "baz")
+}
+`))
+ err := processFiles([]string{fname})
+ c.Assert(err, IsNil)
+
+ c.Assert(msgIDs, DeepEquals, map[string][]msgID{
+ "foo\\nbar\\nbaz": {
+ {
+ comment: "#. TRANSLATORS: foo comment\n",
+ fname: fname,
+ line: 5,
+ },
+ },
+ })
+}
+
+func (s *xgettextTestSuite) TestProcessFilesWithQuote(c *C) {
+ fname := makeGoSourceFile(c, []byte(fmt.Sprintf(`package main
+
+func main() {
+ i18n.G(%[1]s foo "bar"%[1]s)
+}
+`, "`")))
+ err := processFiles([]string{fname})
+ c.Assert(err, IsNil)
+
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#: %[2]s:4
+msgid " foo \"bar\""
+msgstr ""
+
+`, header, fname)
+ c.Check(out.String(), Equals, expected)
+
+}
+
+func (s *xgettextTestSuite) TestWriteOutputMultilines(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo\\nbar\\nbaz": {
+ {
+ fname: "fname",
+ line: 2,
+ comment: "#. foo\n",
+ },
+ },
+ }
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+ expected := fmt.Sprintf(`%s
+#. foo
+#: fname:2
+msgid "foo\n"
+ "bar\n"
+ "baz"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestWriteOutputTidy(c *C) {
+ msgIDs = map[string][]msgID{
+ "foo\\nbar\\nbaz": {
+ {
+ fname: "fname",
+ line: 2,
+ },
+ },
+ "zzz\\n": {
+ {
+ fname: "fname",
+ line: 4,
+ },
+ },
+ }
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+ expected := fmt.Sprintf(`%s
+#: fname:2
+msgid "foo\n"
+ "bar\n"
+ "baz"
+msgstr ""
+
+#: fname:4
+msgid "zzz\n"
+msgstr ""
+
+`, header)
+ c.Assert(out.String(), Equals, expected)
+}
+
+func (s *xgettextTestSuite) TestProcessFilesWithDoubleQuote(c *C) {
+ fname := makeGoSourceFile(c, []byte(`package main
+
+func main() {
+ i18n.G("foo \"bar\"")
+}
+`))
+ err := processFiles([]string{fname})
+ c.Assert(err, IsNil)
+
+ out := bytes.NewBuffer([]byte(""))
+ writePotFile(out)
+
+ expected := fmt.Sprintf(`%s
+#: %[2]s:4
+msgid "foo \"bar\""
+msgstr ""
+
+`, header, fname)
+ c.Check(out.String(), Equals, expected)
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package image
+
+var (
+ LocalSnaps = localSnaps
+ DecodeModelAssertion = decodeModelAssertion
+ DownloadUnpackGadget = downloadUnpackGadget
+ BootstrapToRootDir = bootstrapToRootDir
+ InstallCloudConfig = installCloudConfig
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package image
+
+// TODO: put these in appropriate package(s) once they are clarified a bit more
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+
+ "golang.org/x/net/context"
+)
+
+// DownloadOptions carries options for downloading snaps plus assertions.
+type DownloadOptions struct {
+ TargetDir string
+ Channel string
+ DevMode bool
+ User *auth.UserState
+}
+
+// A Store can find metadata on snaps, download snaps and fetch assertions.
+type Store interface {
+ SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error)
+ Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error
+
+ Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error)
+}
+
+// DownloadSnap downloads the snap with the given name and optionally revision using the provided store and options. It returns the final full path of the snap inside the opts.TargetDir and a snap.Info for the snap.
+func DownloadSnap(sto Store, name string, revision snap.Revision, opts *DownloadOptions) (targetFn string, info *snap.Info, err error) {
+ if opts == nil {
+ opts = &DownloadOptions{}
+ }
+
+ targetDir := opts.TargetDir
+ if targetDir == "" {
+ pwd, err := os.Getwd()
+ if err != nil {
+ return "", nil, err
+ }
+ targetDir = pwd
+ }
+
+ spec := store.SnapSpec{
+ Name: name,
+ Channel: opts.Channel,
+ Revision: revision,
+ }
+ snap, err := sto.SnapInfo(spec, opts.User)
+ if err != nil {
+ return "", nil, fmt.Errorf("cannot find snap %q: %v", name, err)
+ }
+
+ baseName := filepath.Base(snap.MountFile())
+ targetFn = filepath.Join(targetDir, baseName)
+
+ pb := progress.NewTextProgress()
+ if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, opts.User); err != nil {
+ return "", nil, err
+ }
+
+ return targetFn, snap, nil
+}
+
+// StoreAssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them.
+func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher {
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ return sto.Assertion(ref.Type, ref.PrimaryKey, dlOpts.User)
+ }
+ save2 := func(a asserts.Assertion) error {
+ // for checking
+ err := db.Add(a)
+ if err != nil {
+ if _, ok := err.(*asserts.RevisionError); ok {
+ return nil
+ }
+ return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err)
+ }
+ return save(a)
+ }
+ return asserts.NewFetcher(db, retrieve, save2)
+}
+
+// FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database.
+func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) error {
+ sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath)
+ if err != nil {
+ return err
+ }
+
+ if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil {
+ return fmt.Errorf("cannot fetch snap signatures/assertions: %v", err)
+ }
+
+ // cross checks
+ return snapasserts.CrossCheck(info.Name(), sha3_384, size, &info.SideInfo, db)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package image
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/squashfs"
+ "github.com/snapcore/snapd/store"
+)
+
+var (
+ Stdout io.Writer = os.Stdout
+)
+
+type Options struct {
+ Snaps []string
+ RootDir string
+ Channel string
+ ModelFile string
+ GadgetUnpackDir string
+}
+
+type localInfos struct {
+ // path to info for local snaps
+ pathToInfo map[string]*snap.Info
+ // name to path
+ nameToPath map[string]string
+}
+
+func (li *localInfos) Name(pathOrName string) string {
+ if info := li.pathToInfo[pathOrName]; info != nil {
+ return info.Name()
+ }
+ return pathOrName
+}
+
+func (li *localInfos) PreferLocal(name string) string {
+ if path := li.Path(name); path != "" {
+ return path
+ }
+ return name
+}
+
+func (li *localInfos) Path(name string) string {
+ return li.nameToPath[name]
+}
+
+func (li *localInfos) Info(name string) *snap.Info {
+ if p := li.nameToPath[name]; p != "" {
+ return li.pathToInfo[p]
+ }
+ return nil
+}
+
+func localSnaps(opts *Options) (*localInfos, error) {
+ local := make(map[string]*snap.Info)
+ nameToPath := make(map[string]string)
+ for _, snapName := range opts.Snaps {
+ if strings.HasSuffix(snapName, ".snap") && osutil.FileExists(snapName) {
+ snapFile, err := snap.Open(snapName)
+ if err != nil {
+ return nil, err
+ }
+ info, err := snap.ReadInfoFromSnapFile(snapFile, nil)
+ if err != nil {
+ return nil, err
+ }
+ // local snap gets local revision
+ info.Revision = snap.R(-1)
+ nameToPath[info.Name()] = snapName
+ local[snapName] = info
+ }
+ }
+ return &localInfos{
+ pathToInfo: local,
+ nameToPath: nameToPath,
+ }, nil
+}
+
+func Prepare(opts *Options) error {
+ model, err := decodeModelAssertion(opts)
+ if err != nil {
+ return err
+ }
+
+ local, err := localSnaps(opts)
+ if err != nil {
+ return err
+ }
+
+ sto := makeStore(model)
+
+ if err := downloadUnpackGadget(sto, model, opts, local); err != nil {
+ return err
+ }
+
+ return bootstrapToRootDir(sto, model, opts, local)
+}
+
+// these are postponed, not implemented or abandoned, not finalized,
+// don't let them sneak in into a used model assertion
+var reserved = []string{"core", "os", "class", "allowed-modes"}
+
+func decodeModelAssertion(opts *Options) (*asserts.Model, error) {
+ fn := opts.ModelFile
+
+ rawAssert, err := ioutil.ReadFile(fn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read model assertion: %s", err)
+ }
+
+ ass, err := asserts.Decode(rawAssert)
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode model assertion %q: %s", fn, err)
+ }
+ modela, ok := ass.(*asserts.Model)
+ if !ok {
+ return nil, fmt.Errorf("assertion in %q is not a model assertion", fn)
+ }
+
+ for _, rsvd := range reserved {
+ if modela.Header(rsvd) != nil {
+ return nil, fmt.Errorf("model assertion cannot have reserved/unsupported header %q set", rsvd)
+ }
+ }
+
+ return modela, nil
+}
+
+func downloadUnpackGadget(sto Store, model *asserts.Model, opts *Options, local *localInfos) error {
+ if err := os.MkdirAll(opts.GadgetUnpackDir, 0755); err != nil {
+ return fmt.Errorf("cannot create gadget unpack dir %q: %s", opts.GadgetUnpackDir, err)
+ }
+
+ dlOpts := &DownloadOptions{
+ TargetDir: opts.GadgetUnpackDir,
+ Channel: opts.Channel,
+ }
+ snapFn, _, err := acquireSnap(sto, model.Gadget(), dlOpts, local)
+ if err != nil {
+ return err
+ }
+ // FIXME: jumping through layers here, we need to make
+ // unpack part of the container interface (again)
+ snap := squashfs.New(snapFn)
+ return snap.Unpack("*", opts.GadgetUnpackDir)
+}
+
+func acquireSnap(sto Store, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) {
+ if info := local.Info(name); info != nil {
+ // local snap to install (unasserted only for now)
+ p := local.Path(name)
+ dst, err := copyLocalSnapFile(p, dlOpts.TargetDir, info)
+ if err != nil {
+ return "", nil, err
+ }
+ return dst, info, nil
+ }
+ return DownloadSnap(sto, name, snap.R(0), dlOpts)
+}
+
+type addingFetcher struct {
+ asserts.Fetcher
+ addedRefs []*asserts.Ref
+}
+
+func makeFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database) *addingFetcher {
+ var f addingFetcher
+ save := func(a asserts.Assertion) error {
+ f.addedRefs = append(f.addedRefs, a.Ref())
+ return nil
+ }
+ f.Fetcher = StoreAssertionFetcher(sto, dlOpts, db, save)
+ return &f
+
+}
+
+func installCloudConfig(gadgetDir string) error {
+ var err error
+
+ cloudDir := filepath.Join(dirs.GlobalRootDir, "/etc/cloud")
+ if err := os.MkdirAll(cloudDir, 0755); err != nil {
+ return err
+ }
+
+ cloudConfig := filepath.Join(gadgetDir, "cloud.conf")
+ if osutil.FileExists(cloudConfig) {
+ dst := filepath.Join(cloudDir, "cloud.cfg")
+ err = osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite)
+ } else {
+ dst := filepath.Join(cloudDir, "cloud-init.disabled")
+ err = osutil.AtomicWriteFile(dst, nil, 0644, 0)
+ }
+ return err
+}
+
+const defaultCore = "core"
+
+func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *localInfos) error {
+ // FIXME: try to avoid doing this
+ if opts.RootDir != "" {
+ dirs.SetRootDir(opts.RootDir)
+ defer dirs.SetRootDir("/")
+ }
+
+ // sanity check target
+ if osutil.FileExists(dirs.SnapStateFile) {
+ return fmt.Errorf("cannot bootstrap over existing system")
+ }
+
+ // TODO: developer database in home or use snapd (but need
+ // a bit more API there, potential issues when crossing stores/series)
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: sysdb.Trusted(),
+ })
+ if err != nil {
+ return err
+ }
+ f := makeFetcher(sto, &DownloadOptions{}, db)
+
+ if err := f.Save(model); err != nil {
+ if !osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL") {
+ return fmt.Errorf("cannot fetch and check prerequisites for the model assertion: %v", err)
+ } else {
+ logger.Noticef("Cannot fetch and check prerequisites for the model assertion, it will not be copied into the image: %v", err)
+ f.addedRefs = nil
+ }
+ }
+
+ // put snaps in place
+ if err := os.MkdirAll(dirs.SnapBlobDir, 0755); err != nil {
+ return err
+ }
+
+ snapSeedDir := filepath.Join(dirs.SnapSeedDir, "snaps")
+ assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions")
+ dlOpts := &DownloadOptions{
+ TargetDir: snapSeedDir,
+ Channel: opts.Channel,
+ DevMode: false, // XXX: should this be true?
+ }
+
+ for _, d := range []string{snapSeedDir, assertSeedDir} {
+ if err := os.MkdirAll(d, 0755); err != nil {
+ return err
+ }
+ }
+
+ snaps := []string{}
+ // core,kernel,gadget first
+ snaps = append(snaps, local.PreferLocal(defaultCore))
+ snaps = append(snaps, local.PreferLocal(model.Kernel()))
+ snaps = append(snaps, local.PreferLocal(model.Gadget()))
+ // then required and the user requested stuff
+ for _, snapName := range model.RequiredSnaps() {
+ snaps = append(snaps, local.PreferLocal(snapName))
+ }
+ snaps = append(snaps, opts.Snaps...)
+
+ seen := make(map[string]bool)
+ downloadedSnapsInfo := map[string]*snap.Info{}
+ var seedYaml snap.Seed
+ for _, snapName := range snaps {
+ name := local.Name(snapName)
+ if seen[name] {
+ fmt.Fprintf(Stdout, "%s already prepared, skipping\n", name)
+ continue
+ }
+
+ if name != snapName {
+ fmt.Fprintf(Stdout, "Copying %q (%s)\n", snapName, name)
+ } else {
+ fmt.Fprintf(Stdout, "Fetching %s\n", snapName)
+ }
+
+ fn, info, err := acquireSnap(sto, name, dlOpts, local)
+ if err != nil {
+ return err
+ }
+
+ seen[name] = true
+
+ // if it comes from the store fetch the snap assertions too
+ // TODO: support somehow including available assertions
+ // also for local snaps
+ if info.SnapID != "" {
+ err = FetchAndCheckSnapAssertions(fn, info, f, db)
+ if err != nil {
+ return err
+ }
+ }
+
+ typ := info.Type
+ // kernel/os are required for booting
+ if typ == snap.TypeKernel || typ == snap.TypeOS {
+ dst := filepath.Join(dirs.SnapBlobDir, filepath.Base(fn))
+ if err := osutil.CopyFile(fn, dst, 0); err != nil {
+ return err
+ }
+ // store the snap.Info for kernel/os so
+ // that the bootload can DTRT
+ downloadedSnapsInfo[dst] = info
+ }
+
+ // set seed.yaml
+ seedYaml.Snaps = append(seedYaml.Snaps, &snap.SeedSnap{
+ Name: info.Name(),
+ SnapID: info.SnapID, // cross-ref
+ Channel: info.Channel,
+ File: filepath.Base(fn),
+ DevMode: info.NeedsDevMode(),
+ // no assertions for this snap were put in the seed
+ Unasserted: info.SnapID == "",
+ })
+ }
+
+ for _, aRef := range f.addedRefs {
+ var afn string
+ // the names don't matter in practice as long as they don't conflict
+ if aRef.Type == asserts.ModelType {
+ afn = "model"
+ } else {
+ afn = fmt.Sprintf("%s.%s", strings.Join(aRef.PrimaryKey, ","), aRef.Type.Name)
+ }
+ a, err := aRef.Resolve(db.Find)
+ if err != nil {
+ return fmt.Errorf("internal error: lost saved assertion")
+ }
+ err = ioutil.WriteFile(filepath.Join(assertSeedDir, afn), asserts.Encode(a), 0644)
+ if err != nil {
+ return err
+ }
+ }
+
+ // TODO: add the refs as an assertions list of maps section to seed.yaml
+
+ seedFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml")
+ if err := seedYaml.Write(seedFn); err != nil {
+ return fmt.Errorf("cannot write seed.yaml: %s", err)
+ }
+
+ // now do the bootloader stuff
+ if err := partition.InstallBootConfig(opts.GadgetUnpackDir); err != nil {
+ return err
+ }
+
+ if err := setBootvars(downloadedSnapsInfo); err != nil {
+ return err
+ }
+
+ // and the cloud-init things
+ if err := installCloudConfig(opts.GadgetUnpackDir); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func setBootvars(downloadedSnapsInfo map[string]*snap.Info) error {
+ // Set bootvars for kernel/core snaps so the system boots and
+ // does the first-time initialization. There is also no
+ // mounted kernel/core snap, but just the blobs.
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ return fmt.Errorf("cannot set kernel/core boot variables: %s", err)
+ }
+
+ snaps, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, "*.snap"))
+ if len(snaps) == 0 || err != nil {
+ return fmt.Errorf("internal error: cannot find core/kernel snap")
+ }
+
+ m := map[string]string{
+ "snap_mode": "",
+ "snap_try_core": "",
+ "snap_try_kernel": "",
+ }
+ for _, fn := range snaps {
+ bootvar := ""
+
+ info := downloadedSnapsInfo[fn]
+ switch info.Type {
+ case snap.TypeOS:
+ bootvar = "snap_core"
+ case snap.TypeKernel:
+ bootvar = "snap_kernel"
+ if err := extractKernelAssets(fn, info); err != nil {
+ return err
+ }
+ }
+
+ if bootvar != "" {
+ name := filepath.Base(fn)
+ m[bootvar] = name
+ }
+ }
+ if err := bootloader.SetBootVars(m); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func runCommand(cmdStr ...string) error {
+ cmd := exec.Command(cmdStr[0], cmdStr[1:]...)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("cannot run %v: %s", cmdStr, osutil.OutputErr(output, err))
+ }
+ return nil
+}
+
+func extractKernelAssets(snapPath string, info *snap.Info) error {
+ snapf, err := snap.Open(snapPath)
+ if err != nil {
+ return err
+ }
+
+ if err := boot.ExtractKernelAssets(info, snapf); err != nil {
+ return err
+ }
+ return nil
+}
+
+func copyLocalSnapFile(snapPath, targetDir string, info *snap.Info) (dstPath string, err error) {
+ dst := filepath.Join(targetDir, filepath.Base(info.MountFile()))
+ return dst, osutil.CopyFile(snapPath, dst, 0)
+}
+
+func makeStore(model *asserts.Model) Store {
+ cfg := store.DefaultConfig()
+ cfg.Architecture = model.Architecture()
+ cfg.Series = model.Series()
+ cfg.StoreID = model.Store()
+ return store.New(cfg, nil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package image_test
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/image"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type imageSuite struct {
+ root string
+ bootloader *boottest.MockBootloader
+
+ stdout *bytes.Buffer
+
+ downloadedSnaps map[string]string
+ storeSnapInfo map[string]*snap.Info
+
+ storeSigning *assertstest.StoreStack
+ brandSigning *assertstest.SigningDB
+
+ model *asserts.Model
+}
+
+var _ = Suite(&imageSuite{})
+
+func (s *imageSuite) SetUpTest(c *C) {
+ s.root = c.MkDir()
+ s.bootloader = boottest.NewMockBootloader("grub", c.MkDir())
+ partition.ForceBootloader(s.bootloader)
+
+ s.stdout = bytes.NewBuffer(nil)
+ image.Stdout = s.stdout
+ s.downloadedSnaps = make(map[string]string)
+ s.storeSnapInfo = make(map[string]*snap.Info)
+
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+
+ brandPrivKey, _ := assertstest.GenerateKey(752)
+ s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey)
+
+ brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
+ "account-id": "my-brand",
+ "verification": "certified",
+ }, "")
+ s.storeSigning.Add(brandAcct)
+
+ brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, brandPrivKey.PublicKey(), "")
+ s.storeSigning.Add(brandAccKey)
+
+ model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "architecture": "amd64",
+ "gadget": "pc",
+ "kernel": "pc-kernel",
+ "required-snaps": []interface{}{"required-snap1"},
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ s.model = model.(*asserts.Model)
+}
+
+func (s *imageSuite) addSystemSnapAssertions(c *C, snapName string) {
+ snapID := snapName + "-Id"
+ decl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": snapID,
+ "snap-name": snapName,
+ "publisher-id": "can0nical",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(decl)
+ c.Assert(err, IsNil)
+
+ snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(s.downloadedSnaps[snapName])
+ c.Assert(err, IsNil)
+
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": snapSHA3_384,
+ "snap-size": fmt.Sprintf("%d", snapSize),
+ "snap-id": snapID,
+ "snap-revision": s.storeSnapInfo[snapName].Revision.String(),
+ "developer-id": "can0nical",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapRev)
+ c.Assert(err, IsNil)
+}
+
+func (s *imageSuite) TearDownTest(c *C) {
+ partition.ForceBootloader(nil)
+ image.Stdout = os.Stdout
+}
+
+// interface for the store
+func (s *imageSuite) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
+ return s.storeSnapInfo[spec.Name], nil
+}
+
+func (s *imageSuite) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error {
+ return osutil.CopyFile(s.downloadedSnaps[name], targetFn, 0)
+}
+
+func (s *imageSuite) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) {
+ ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey}
+ return ref.Resolve(s.storeSigning.Find)
+}
+
+const packageGadget = `
+name: pc
+version: 1.0
+type: gadget
+`
+
+const packageKernel = `
+name: pc-kernel
+version: 4.4-1
+type: kernel
+`
+
+const packageCore = `
+name: core
+version: 16.04
+type: os
+`
+
+const devmodeSnap = `
+name: devmode-snap
+version: 1.0
+type: app
+confinement: devmode
+`
+
+const requiredSnap1 = `
+name: required-snap1
+version: 1.0
+`
+
+func (s *imageSuite) TestMissingModelAssertions(c *C) {
+ _, err := image.DecodeModelAssertion(&image.Options{})
+ c.Assert(err, ErrorMatches, "cannot read model assertion: open : no such file or directory")
+}
+
+func (s *imageSuite) TestIncorrectModelAssertions(c *C) {
+ fn := filepath.Join(c.MkDir(), "broken-model.assertion")
+ err := ioutil.WriteFile(fn, nil, 0644)
+ c.Assert(err, IsNil)
+ _, err = image.DecodeModelAssertion(&image.Options{
+ ModelFile: fn,
+ })
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot decode model assertion "%s": assertion content/signature separator not found`, fn))
+}
+
+func (s *imageSuite) TestValidButDifferentAssertion(c *C) {
+ var differentAssertion = []byte(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-id: snap-id-1
+snap-name: first
+publisher-id: dev-id1
+timestamp: 2016-01-02T10:00:00-05:00
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==
+`)
+
+ fn := filepath.Join(c.MkDir(), "different.assertion")
+ err := ioutil.WriteFile(fn, differentAssertion, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = image.DecodeModelAssertion(&image.Options{
+ ModelFile: fn,
+ })
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`assertion in "%s" is not a model assertion`, fn))
+}
+
+func (s *imageSuite) TestModelAssertionReservedHeaders(c *C) {
+ const mod = `type: model
+authority-id: brand
+series: 16
+brand-id: brand
+model: baz-3000
+architecture: armhf
+gadget: brand-gadget
+kernel: kernel
+timestamp: 2016-01-02T10:00:00-05:00
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==
+`
+
+ reserved := []string{
+ "core",
+ "os",
+ "class",
+ "allowed-modes",
+ }
+
+ for _, rsvd := range reserved {
+ tweaked := strings.Replace(mod, "kernel: kernel\n", fmt.Sprintf("kernel: kernel\n%s: stuff\n", rsvd), 1)
+ fn := filepath.Join(c.MkDir(), "model.assertion")
+ err := ioutil.WriteFile(fn, []byte(tweaked), 0644)
+ c.Assert(err, IsNil)
+ _, err = image.DecodeModelAssertion(&image.Options{
+ ModelFile: fn,
+ })
+ c.Check(err, ErrorMatches, fmt.Sprintf("model assertion cannot have reserved/unsupported header %q set", rsvd))
+ }
+}
+
+func (s *imageSuite) TestHappyDecodeModelAssertion(c *C) {
+ fn := filepath.Join(c.MkDir(), "model.assertion")
+ err := ioutil.WriteFile(fn, asserts.Encode(s.model), 0644)
+ c.Assert(err, IsNil)
+
+ a, err := image.DecodeModelAssertion(&image.Options{
+ ModelFile: fn,
+ })
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+}
+
+func (s *imageSuite) TestMissingGadgetUnpackDir(c *C) {
+ err := image.DownloadUnpackGadget(s, s.model, &image.Options{}, nil)
+ c.Assert(err, ErrorMatches, `cannot create gadget unpack dir "": mkdir : no such file or directory`)
+}
+
+func infoFromSnapYaml(c *C, snapYaml string, rev snap.Revision) *snap.Info {
+ info, err := snap.InfoFromSnapYaml([]byte(snapYaml))
+ c.Assert(err, IsNil)
+
+ if !rev.Unset() {
+ info.SnapID = info.Name() + "-Id"
+ info.Revision = rev
+ }
+ return info
+}
+
+func (s *imageSuite) TestDownloadUnpackGadget(c *C) {
+ files := [][]string{
+ {"subdir/canary.txt", "I'm a canary"},
+ }
+ s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, files)
+ s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(99))
+
+ gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget-unpack-dir")
+ opts := &image.Options{
+ GadgetUnpackDir: gadgetUnpackDir,
+ }
+ local, err := image.LocalSnaps(opts)
+ c.Assert(err, IsNil)
+
+ err = image.DownloadUnpackGadget(s, s.model, opts, local)
+ c.Assert(err, IsNil)
+
+ // verify the right data got unpacked
+ for _, t := range []struct{ file, content string }{
+ {"meta/snap.yaml", packageGadget},
+ {files[0][0], files[0][1]},
+ } {
+ fn := filepath.Join(gadgetUnpackDir, t.file)
+ content, err := ioutil.ReadFile(fn)
+ c.Assert(err, IsNil)
+ c.Check(content, DeepEquals, []byte(t.content))
+ }
+}
+
+func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string) {
+ err := os.MkdirAll(gadgetUnpackDir, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644)
+ c.Assert(err, IsNil)
+
+ s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, [][]string{{"grub.cfg", "I'm a grub.cfg"}})
+ s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(1))
+ s.addSystemSnapAssertions(c, "pc")
+
+ s.downloadedSnaps["pc-kernel"] = snaptest.MakeTestSnapWithFiles(c, packageKernel, nil)
+ s.storeSnapInfo["pc-kernel"] = infoFromSnapYaml(c, packageKernel, snap.R(2))
+ s.addSystemSnapAssertions(c, "pc-kernel")
+
+ s.downloadedSnaps["core"] = snaptest.MakeTestSnapWithFiles(c, packageCore, nil)
+ s.storeSnapInfo["core"] = infoFromSnapYaml(c, packageCore, snap.R(3))
+ s.addSystemSnapAssertions(c, "core")
+
+ s.downloadedSnaps["required-snap1"] = snaptest.MakeTestSnapWithFiles(c, requiredSnap1, nil)
+ s.storeSnapInfo["required-snap1"] = infoFromSnapYaml(c, requiredSnap1, snap.R(3))
+ s.addSystemSnapAssertions(c, "required-snap1")
+}
+
+func (s *imageSuite) TestBootstrapToRootDir(c *C) {
+ restore := sysdb.InjectTrusted(s.storeSigning.Trusted)
+ defer restore()
+
+ rootdir := filepath.Join(c.MkDir(), "imageroot")
+
+ // FIXME: bootstrapToRootDir needs an unpacked gadget yaml
+ gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget")
+
+ s.setupSnaps(c, gadgetUnpackDir)
+
+ // mock the mount cmds (for the extract kernel assets stuff)
+ c1 := testutil.MockCommand(c, "mount", "")
+ defer c1.Restore()
+ c2 := testutil.MockCommand(c, "umount", "")
+ defer c2.Restore()
+
+ opts := &image.Options{
+ RootDir: rootdir,
+ GadgetUnpackDir: gadgetUnpackDir,
+ }
+ local, err := image.LocalSnaps(opts)
+ c.Assert(err, IsNil)
+
+ err = image.BootstrapToRootDir(s, s.model, opts, local)
+ c.Assert(err, IsNil)
+
+ // check seed yaml
+ seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ c.Assert(err, IsNil)
+
+ c.Check(seed.Snaps, HasLen, 4)
+
+ // check the files are in place
+ for i, name := range []string{"core", "pc-kernel", "pc"} {
+ info := s.storeSnapInfo[name]
+ fn := filepath.Base(info.MountFile())
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+
+ c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ Name: name,
+ SnapID: name + "-Id",
+ File: fn,
+ })
+ }
+
+ storeAccountKey := s.storeSigning.StoreAccountKey("")
+ brandPubKey, err := s.brandSigning.PublicKey("")
+ c.Assert(err, IsNil)
+
+ // check the assertions are in place
+ for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} {
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/assertions", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+ }
+
+ b, err := ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "model"))
+ c.Assert(err, IsNil)
+ c.Check(b, DeepEquals, asserts.Encode(s.model))
+
+ b, err = ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "my-brand.account"))
+ c.Assert(err, IsNil)
+ a, err := asserts.Decode(b)
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.AccountType)
+ c.Check(a.HeaderString("account-id"), Equals, "my-brand")
+
+ // check the snap assertions are also in place
+ for _, snapId := range []string{"pc-Id", "pc-kernel-Id", "core-Id"} {
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/assertions", fmt.Sprintf("16,%s.snap-declaration", snapId))
+ c.Check(osutil.FileExists(p), Equals, true)
+ }
+
+ // check the bootloader config
+ m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
+ c.Assert(err, IsNil)
+ c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
+ c.Check(m["snap_core"], Equals, "core_3.snap")
+}
+
+func (s *imageSuite) TestBootstrapToRootDirLocalCore(c *C) {
+ restore := sysdb.InjectTrusted(s.storeSigning.Trusted)
+ defer restore()
+
+ rootdir := filepath.Join(c.MkDir(), "imageroot")
+
+ // FIXME: bootstrapToRootDir needs an unpacked gadget yaml
+ gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget")
+
+ s.setupSnaps(c, gadgetUnpackDir)
+
+ // mock the mount cmds (for the extract kernel assets stuff)
+ c1 := testutil.MockCommand(c, "mount", "")
+ defer c1.Restore()
+ c2 := testutil.MockCommand(c, "umount", "")
+ defer c2.Restore()
+
+ opts := &image.Options{
+ Snaps: []string{
+ s.downloadedSnaps["core"],
+ s.downloadedSnaps["required-snap1"],
+ },
+ RootDir: rootdir,
+ GadgetUnpackDir: gadgetUnpackDir,
+ }
+ local, err := image.LocalSnaps(opts)
+ c.Assert(err, IsNil)
+
+ err = image.BootstrapToRootDir(s, s.model, opts, local)
+ c.Assert(err, IsNil)
+
+ // check seed yaml
+ seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ c.Assert(err, IsNil)
+
+ c.Check(seed.Snaps, HasLen, 4)
+
+ // check the files are in place
+ for i, name := range []string{"core_x1.snap", "pc-kernel", "pc", "required-snap1_x1.snap"} {
+ unasserted := false
+ info := s.storeSnapInfo[name]
+ if info == nil {
+ switch name {
+ case "core_x1.snap":
+ info = &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "core",
+ Revision: snap.R("x1"),
+ },
+ }
+ unasserted = true
+ case "required-snap1_x1.snap":
+ info = &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "required-snap1",
+ Revision: snap.R("x1"),
+ },
+ }
+ unasserted = true
+ }
+ }
+
+ fn := filepath.Base(info.MountFile())
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+
+ c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ Name: info.Name(),
+ SnapID: info.SnapID,
+ File: fn,
+ Unasserted: unasserted,
+ })
+ }
+
+ l, err := ioutil.ReadDir(filepath.Join(rootdir, "var/lib/snapd/seed/snaps"))
+ c.Assert(err, IsNil)
+ c.Check(l, HasLen, 4)
+
+ storeAccountKey := s.storeSigning.StoreAccountKey("")
+ brandPubKey, err := s.brandSigning.PublicKey("")
+ c.Assert(err, IsNil)
+
+ // check the assertions are in place
+ for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} {
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/assertions", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+ }
+
+ b, err := ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "model"))
+ c.Assert(err, IsNil)
+ c.Check(b, DeepEquals, asserts.Encode(s.model))
+
+ b, err = ioutil.ReadFile(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "my-brand.account"))
+ c.Assert(err, IsNil)
+ a, err := asserts.Decode(b)
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.AccountType)
+ c.Check(a.HeaderString("account-id"), Equals, "my-brand")
+
+ decls, err := filepath.Glob(filepath.Join(rootdir, "var/lib/snapd/seed/assertions", "*.snap-declaration"))
+ c.Assert(err, IsNil)
+ // nothing for core
+ c.Check(decls, HasLen, 2)
+
+ // check the bootloader config
+ m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
+ c.Assert(err, IsNil)
+ c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
+ c.Assert(err, IsNil)
+ c.Check(m["snap_core"], Equals, "core_x1.snap")
+
+ // check that cloud-init is setup correctly
+ c.Check(osutil.FileExists(filepath.Join(rootdir, "etc/cloud/cloud-init.disabled")), Equals, true)
+}
+
+func (s *imageSuite) TestBootstrapToRootDirDevmodeSnap(c *C) {
+ restore := sysdb.InjectTrusted(s.storeSigning.Trusted)
+ defer restore()
+
+ rootdir := filepath.Join(c.MkDir(), "imageroot")
+
+ // FIXME: bootstrapToRootDir needs an unpacked gadget yaml
+ gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget")
+
+ err := os.MkdirAll(gadgetUnpackDir, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644)
+ c.Assert(err, IsNil)
+
+ s.setupSnaps(c, gadgetUnpackDir)
+
+ s.downloadedSnaps["devmode-snap"] = snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil)
+ s.storeSnapInfo["devmode-snap"] = infoFromSnapYaml(c, devmodeSnap, snap.R(0))
+
+ // mock the mount cmds (for the extract kernel assets stuff)
+ c1 := testutil.MockCommand(c, "mount", "")
+ defer c1.Restore()
+ c2 := testutil.MockCommand(c, "umount", "")
+ defer c2.Restore()
+
+ opts := &image.Options{
+ Snaps: []string{s.downloadedSnaps["devmode-snap"]},
+
+ RootDir: rootdir,
+ GadgetUnpackDir: gadgetUnpackDir,
+ }
+ local, err := image.LocalSnaps(opts)
+ c.Assert(err, IsNil)
+
+ err = image.BootstrapToRootDir(s, s.model, opts, local)
+ c.Assert(err, IsNil)
+
+ // check seed yaml
+ seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ c.Assert(err, IsNil)
+
+ c.Check(seed.Snaps, HasLen, 5)
+
+ // check devmode-snap
+ info := &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "devmode-snap",
+ Revision: snap.R("x1"),
+ },
+ }
+ fn := filepath.Base(info.MountFile())
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+
+ // ensure local snaps are put last in seed.yaml
+ last := len(seed.Snaps) - 1
+ c.Check(seed.Snaps[last], DeepEquals, &snap.SeedSnap{
+ Name: "devmode-snap",
+ File: fn,
+ DevMode: true,
+ Unasserted: true,
+ })
+}
+
+func (s *imageSuite) TestInstallCloudConfigNoConfig(c *C) {
+ targetDir := c.MkDir()
+ emptyGadgetDir := c.MkDir()
+
+ dirs.SetRootDir(targetDir)
+ err := image.InstallCloudConfig(emptyGadgetDir)
+ c.Assert(err, IsNil)
+ c.Check(osutil.FileExists(filepath.Join(targetDir, "etc/cloud/cloud-init.disabled")), Equals, true)
+}
+
+func (s *imageSuite) TestInstallCloudConfigWithCloudConfig(c *C) {
+ canary := []byte("ni! ni! ni!")
+
+ targetDir := c.MkDir()
+ gadgetDir := c.MkDir()
+ err := ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), canary, 0644)
+ c.Assert(err, IsNil)
+
+ dirs.SetRootDir(targetDir)
+ err = image.InstallCloudConfig(gadgetDir)
+ c.Assert(err, IsNil)
+ content, err := ioutil.ReadFile(filepath.Join(targetDir, "etc/cloud/cloud.cfg"))
+ c.Assert(err, IsNil)
+ c.Check(content, DeepEquals, canary)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package apparmor contains primitives for working with apparmor.
+//
+// References:
+// - http://wiki.apparmor.net/index.php/Kernel_interfaces
+// - http://apparmor.wiki.kernel.org/
+// - http://manpages.ubuntu.com/manpages/xenial/en/man7/apparmor.7.html
+package apparmor
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+)
+
+// LoadProfile loads an apparmor profile from the given file.
+//
+// If no such profile was previously loaded then it is simply added to the kernel.
+// If there was a profile with the same name before, that profile is replaced.
+func LoadProfile(fname string) error {
+ // Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858)
+ output, err := exec.Command(
+ "apparmor_parser", "--replace", "--write-cache", "-O",
+ "no-expr-simplify", fmt.Sprintf("--cache-loc=%s", dirs.AppArmorCacheDir),
+ fname).CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot load apparmor profile: %s\napparmor_parser output:\n%s", err, string(output))
+ }
+ return nil
+}
+
+// UnloadProfile removes the named profile from the running kernel.
+//
+// The operation is done with: apparmor_parser --remove $name
+// The binary cache file is removed from /var/cache/apparmor
+func UnloadProfile(name string) error {
+ output, err := exec.Command("apparmor_parser", "--remove", name).CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot unload apparmor profile: %s\napparmor_parser output:\n%s", err, string(output))
+ }
+ err = os.Remove(filepath.Join(dirs.AppArmorCacheDir, name))
+ // It is not an error if the cache file wasn't there to remove.
+ if err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("cannot remove apparmor profile cache: %s", err)
+ }
+ return nil
+}
+
+// profilesPath contains information about the currently loaded apparmor profiles.
+const realProfilesPath = "/sys/kernel/security/apparmor/profiles"
+
+var profilesPath = realProfilesPath
+
+// LoadedProfiles interrogates the kernel and returns a list of loaded apparmor profiles.
+//
+// Snappy manages apparmor profiles named "snap.*". Other profiles might exist on
+// the system (via snappy dimension) and those are filtered-out.
+func LoadedProfiles() ([]string, error) {
+ file, err := os.Open(profilesPath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ var profiles []string
+ for {
+ var name, mode string
+ n, err := fmt.Fscanf(file, "%s %s\n", &name, &mode)
+ if n > 0 && n != 2 {
+ return nil, fmt.Errorf("syntax error, expected: name (mode)")
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(name, "snap.") {
+ profiles = append(profiles, name)
+ }
+ }
+ return profiles, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package apparmor_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type appArmorSuite struct {
+ testutil.BaseTest
+ profilesFilename string
+}
+
+var _ = Suite(&appArmorSuite{})
+
+func (s *appArmorSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+ // Mock the list of profiles in the running kernel
+ s.profilesFilename = path.Join(c.MkDir(), "profiles")
+ apparmor.MockProfilesPath(&s.BaseTest, s.profilesFilename)
+}
+
+// Tests for LoadProfile()
+
+func (s *appArmorSuite) TestLoadProfileRunsAppArmorParserReplace(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+ err := apparmor.LoadProfile("/path/to/snap.samba.smbd")
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "/path/to/snap.samba.smbd"},
+ })
+}
+
+func (s *appArmorSuite) TestLoadProfileReportsErrors(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42")
+ defer cmd.Restore()
+ err := apparmor.LoadProfile("/path/to/snap.samba.smbd")
+ c.Assert(err.Error(), Equals, `cannot load apparmor profile: exit status 42
+apparmor_parser output:
+`)
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "/path/to/snap.samba.smbd"},
+ })
+}
+
+// Tests for Profile.Unload()
+
+func (s *appArmorSuite) TestUnloadProfileRunsAppArmorParserRemove(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+ err := apparmor.UnloadProfile("snap.samba.smbd")
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--remove", "snap.samba.smbd"},
+ })
+}
+
+func (s *appArmorSuite) TestUnloadProfileReportsErrors(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42")
+ defer cmd.Restore()
+ err := apparmor.UnloadProfile("snap.samba.smbd")
+ c.Assert(err.Error(), Equals, `cannot unload apparmor profile: exit status 42
+apparmor_parser output:
+`)
+}
+
+func (s *appArmorSuite) TestUnloadRemovesCachedProfile(c *C) {
+ cmd := testutil.MockCommand(c, "apparmor_parser", "")
+ defer cmd.Restore()
+
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+ err := os.MkdirAll(dirs.AppArmorCacheDir, 0755)
+ c.Assert(err, IsNil)
+
+ fname := filepath.Join(dirs.AppArmorCacheDir, "profile")
+ ioutil.WriteFile(fname, []byte("blob"), 0600)
+ err = apparmor.UnloadProfile("profile")
+ c.Assert(err, IsNil)
+ _, err = os.Stat(fname)
+ c.Check(os.IsNotExist(err), Equals, true)
+}
+
+// Tests for LoadedProfiles()
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesReturnsErrorOnMissingFile(c *C) {
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, ErrorMatches, "open .*: no such file or directory")
+ c.Check(profiles, IsNil)
+}
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesCanParseEmptyFile(c *C) {
+ ioutil.WriteFile(s.profilesFilename, []byte(""), 0600)
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, IsNil)
+ c.Check(profiles, HasLen, 0)
+}
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesParsesAndFiltersData(c *C) {
+ ioutil.WriteFile(s.profilesFilename, []byte(
+ // The output contains some of the snappy-specific elements
+ // and some non-snappy elements pulled from Ubuntu 16.04 desktop
+ //
+ // The pi2-piglow.{background,foreground}.snap entries are the only
+ // ones that should be reported by the function.
+ `/sbin/dhclient (enforce)
+/usr/bin/ubuntu-core-launcher (enforce)
+/usr/bin/ubuntu-core-launcher (enforce)
+/usr/lib/NetworkManager/nm-dhcp-client.action (enforce)
+/usr/lib/NetworkManager/nm-dhcp-helper (enforce)
+/usr/lib/connman/scripts/dhclient-script (enforce)
+/usr/lib/lightdm/lightdm-guest-session (enforce)
+/usr/lib/lightdm/lightdm-guest-session//chromium (enforce)
+/usr/lib/telepathy/telepathy-* (enforce)
+/usr/lib/telepathy/telepathy-*//pxgsettings (enforce)
+/usr/lib/telepathy/telepathy-*//sanitized_helper (enforce)
+snap.pi2-piglow.background (enforce)
+snap.pi2-piglow.foreground (enforce)
+webbrowser-app (enforce)
+webbrowser-app//oxide_helper (enforce)
+`), 0600)
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, IsNil)
+ c.Check(profiles, DeepEquals, []string{
+ "snap.pi2-piglow.background",
+ "snap.pi2-piglow.foreground",
+ })
+}
+
+func (s *appArmorSuite) TestLoadedApparmorProfilesHandlesParsingErrors(c *C) {
+ ioutil.WriteFile(s.profilesFilename, []byte("broken stuff here\n"), 0600)
+ profiles, err := apparmor.LoadedProfiles()
+ c.Assert(err, ErrorMatches, "newline in format does not match input")
+ c.Check(profiles, IsNil)
+ ioutil.WriteFile(s.profilesFilename, []byte("truncated"), 0600)
+ profiles, err = apparmor.LoadedProfiles()
+ c.Assert(err, ErrorMatches, `syntax error, expected: name \(mode\)`)
+ c.Check(profiles, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package apparmor implements integration between snappy and
+// ubuntu-core-launcher around apparmor.
+//
+// Snappy creates apparmor profiles for each application (for each snap)
+// present in the system. Upon each execution of ubuntu-core-launcher
+// application process is launched under the profile. Prior to that the profile
+// must be parsed, compiled and loaded into the kernel using the support tool
+// "apparmor_parser".
+//
+// Each apparmor profile contains a simple <header><content><footer> structure.
+// The header specifies the profile name that the launcher will use to launch a
+// process under this profile. Snappy uses "abstract identifiers" as profile
+// names.
+//
+// The actual profiles are stored in /var/lib/snappy/apparmor/profiles.
+//
+// NOTE: A systemd job (apparmor.service) loads all snappy-specific apparmor
+// profiles into the kernel during the boot process.
+package apparmor
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining apparmor profiles for ubuntu-core-launcher.
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "apparmor"
+}
+
+// Setup creates and loads apparmor profiles specific to a given snap.
+// The snap can be in developer mode to make security violations non-fatal to
+// the offending application process.
+//
+// This method should be called after changing plug, slots, connections between
+// them or application present in the snap.
+func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ // Get the snippets that apply to this snap
+ snippets, err := repo.SecuritySnippetsForSnap(snapName, interfaces.SecurityAppArmor)
+ if err != nil {
+ return fmt.Errorf("cannot obtain security snippets for snap %q: %s", snapName, err)
+ }
+ // Get the files that this snap should have
+ content, err := b.combineSnippets(snapInfo, opts, snippets)
+ if err != nil {
+ return fmt.Errorf("cannot obtain expected security files for snap %q: %s", snapName, err)
+ }
+ glob := interfaces.SecurityTagGlob(snapInfo.Name())
+ dir := dirs.SnapAppArmorDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for apparmor profiles %q: %s", dir, err)
+ }
+ _, removed, errEnsure := osutil.EnsureDirState(dir, glob, content)
+ // NOTE: load all profiles instead of just the changed profiles. We're
+ // relying on apparmor cache to make this efficient. This gives us
+ // certainty that each call to Setup ends up with working profiles.
+ all := make([]string, 0, len(content))
+ for name := range content {
+ all = append(all, name)
+ }
+ sort.Strings(all)
+ errReload := reloadProfiles(all)
+ errUnload := unloadProfiles(removed)
+ if errEnsure != nil {
+ return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure)
+ }
+ if errReload != nil {
+ return errReload
+ }
+ return errUnload
+}
+
+// Remove removes and unloads apparmor profiles of a given snap.
+func (b *Backend) Remove(snapName string) error {
+ glob := interfaces.SecurityTagGlob(snapName)
+ _, removed, errEnsure := osutil.EnsureDirState(dirs.SnapAppArmorDir, glob, nil)
+ errUnload := unloadProfiles(removed)
+ if errEnsure != nil {
+ return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure)
+ }
+ return errUnload
+}
+
+var (
+ templatePattern = regexp.MustCompile("(###[A-Z]+###)")
+ placeholderVar = []byte("###VAR###")
+ placeholderSnippets = []byte("###SNIPPETS###")
+ placeholderProfileAttach = []byte("###PROFILEATTACH###")
+ attachPattern = regexp.MustCompile(`\(attach_disconnected\)`)
+ attachComplain = []byte("(attach_disconnected,complain)")
+)
+
+// combineSnippets combines security snippets collected from all the interfaces
+// affecting a given snap into a content map applicable to EnsureDirState. The
+// backend delegates writing those files to higher layers.
+func (b *Backend) combineSnippets(snapInfo *snap.Info, opts interfaces.ConfinementOptions, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) {
+ for _, appInfo := range snapInfo.Apps {
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+ addContent(appInfo.SecurityTag(), snapInfo, opts, snippets, content)
+ }
+
+ for _, hookInfo := range snapInfo.Hooks {
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+ addContent(hookInfo.SecurityTag(), snapInfo, opts, snippets, content)
+ }
+
+ return content, nil
+}
+
+func addContent(securityTag string, snapInfo *snap.Info, opts interfaces.ConfinementOptions, snippets map[string][][]byte, content map[string]*osutil.FileState) {
+ var policy []byte
+ if opts.Classic && !opts.JailMode {
+ policy = classicTemplate
+ } else {
+ policy = defaultTemplate
+ }
+ if (opts.DevMode || opts.Classic) && !opts.JailMode {
+ policy = attachPattern.ReplaceAll(policy, attachComplain)
+ }
+ policy = templatePattern.ReplaceAllFunc(policy, func(placeholder []byte) []byte {
+ switch {
+ case bytes.Equal(placeholder, placeholderVar):
+ return templateVariables(snapInfo)
+ case bytes.Equal(placeholder, placeholderProfileAttach):
+ return []byte(fmt.Sprintf("profile \"%s\"", securityTag))
+ case bytes.Equal(placeholder, placeholderSnippets):
+ var tagSnippets [][]byte
+
+ if opts.Classic && opts.JailMode {
+ // Add a special internal snippet for snaps using classic confinement
+ // and jailmode together. This snippet provides access to the core snap
+ // so that the dynamic linker and shared libraries can be used.
+ tagSnippets = append(tagSnippets, classicJailmodeSnippet)
+ tagSnippets = append(tagSnippets, snippets[securityTag]...)
+ } else if opts.Classic && !opts.JailMode {
+ // When classic confinement (without jailmode) is in effect we
+ // are ignoring all apparmor snippets as they may conflict with
+ // the super-broad template we are starting with.
+ } else {
+ tagSnippets = snippets[securityTag]
+ }
+ return bytes.Join(tagSnippets, []byte("\n"))
+ }
+ return nil
+ })
+
+ content[securityTag] = &osutil.FileState{
+ Content: policy,
+ Mode: 0644,
+ }
+}
+
+func reloadProfiles(profiles []string) error {
+ for _, profile := range profiles {
+ fname := filepath.Join(dirs.SnapAppArmorDir, profile)
+ err := LoadProfile(fname)
+ if err != nil {
+ return fmt.Errorf("cannot load apparmor profile %q: %s", profile, err)
+ }
+ }
+ return nil
+}
+
+func unloadProfiles(profiles []string) error {
+ for _, profile := range profiles {
+ if err := UnloadProfile(profile); err != nil {
+ return fmt.Errorf("cannot unload apparmor profile %q: %s", profile, err)
+ }
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package apparmor_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+
+ parserCmd *testutil.MockCmd
+}
+
+var _ = Suite(&backendSuite{})
+
+var testedConfinementOpts = []interfaces.ConfinementOptions{
+ {},
+ {DevMode: true},
+ {JailMode: true},
+ {Classic: true},
+}
+
+// fakeAppAprmorParser contains shell program that creates fake binary cache entries
+// in accordance with what real apparmor_parser would do.
+const fakeAppArmorParser = `
+cache_dir=""
+profile=""
+write=""
+while [ -n "$1" ]; do
+ case "$1" in
+ --cache-loc=*)
+ cache_dir="$(echo "$1" | cut -d = -f 2)" || exit 1
+ ;;
+ --write-cache)
+ write=yes
+ ;;
+ --replace|--remove)
+ # Ignore
+ ;;
+ -O)
+ # Ignore, discard argument
+ shift
+ ;;
+ *)
+ profile=$(basename "$1")
+ ;;
+ esac
+ shift
+done
+if [ "$write" = yes ]; then
+ echo fake > "$cache_dir/$profile"
+fi
+`
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.Backend = &apparmor.Backend{}
+ s.BackendSuite.SetUpTest(c)
+
+ // Prepare a directory for apparmor profiles.
+ // NOTE: Normally this is a part of the OS snap.
+ err := os.MkdirAll(dirs.SnapAppArmorDir, 0700)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll(dirs.AppArmorCacheDir, 0700)
+ c.Assert(err, IsNil)
+ // Mock away any real apparmor interaction
+ s.parserCmd = testutil.MockCommand(c, "apparmor_parser", fakeAppArmorParser)
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.parserCmd.Restore()
+
+ s.BackendSuite.TearDownTest(c)
+}
+
+// Tests for Setup() and Remove()
+
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "apparmor")
+}
+
+func (s *backendSuite) TestInstallingSnapWritesAndLoadsProfiles(c *C) {
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ // file called "snap.sambda.smbd" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+ // apparmor_parser was used to load that file
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), profile},
+ })
+}
+
+func (s *backendSuite) TestInstallingSnapWithHookWritesAndLoadsProfiles(c *C) {
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, ifacetest.HookYaml, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.foo.hook.configure")
+
+ // Verify that profile "snap.foo.hook.configure" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+ // apparmor_parser was used to load that file
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), profile},
+ })
+}
+
+func (s *backendSuite) TestProfilesAreAlwaysLoaded(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 1)
+ s.parserCmd.ForgetCalls()
+ err := s.Backend.Setup(snapInfo, opts, s.Repo)
+ c.Assert(err, IsNil)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), profile},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesAndUnloadsProfiles(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 1)
+ s.parserCmd.ForgetCalls()
+ s.RemoveSnap(c, snapInfo)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ // file called "snap.sambda.smbd" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // apparmor cache file was removed
+ cache := filepath.Join(dirs.AppArmorCacheDir, "snap.samba.smbd")
+ _, err = os.Stat(cache)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // apparmor_parser was used to unload the profile
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--remove", "snap.samba.smbd"},
+ })
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapWithHookRemovesAndUnloadsProfiles(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.HookYaml, 1)
+ s.parserCmd.ForgetCalls()
+ s.RemoveSnap(c, snapInfo)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.foo.hook.configure")
+ // file called "snap.foo.hook.configure" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // apparmor cache file was removed
+ cache := filepath.Join(dirs.AppArmorCacheDir, "snap.foo.hook.configure")
+ _, err = os.Stat(cache)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // apparmor_parser was used to unload the profile
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--remove", "snap.foo.hook.configure"},
+ })
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapMakesNeccesaryChanges(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 1)
+ s.parserCmd.ForgetCalls()
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 2)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ // apparmor_parser was used to reload the profile because snap revision
+ // is inside the generated policy.
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), profile},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 1)
+ s.parserCmd.ForgetCalls()
+ // NOTE: the revision is kept the same to just test on the new application being added
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 1)
+ smbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ nmbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.nmbd")
+ // file called "snap.sambda.nmbd" was created
+ _, err := os.Stat(nmbdProfile)
+ c.Check(err, IsNil)
+ // apparmor_parser was used to load the both profiles
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), nmbdProfile},
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), smbdProfile},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1WithNmbd, 1)
+ s.parserCmd.ForgetCalls()
+ // NOTE: the revision is kept the same to just test on the new application being added
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlWithHook, 1)
+ smbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ nmbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.nmbd")
+ hookProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.hook.configure")
+
+ // Verify that profile "snap.samba.hook.configure" was created
+ _, err := os.Stat(hookProfile)
+ c.Check(err, IsNil)
+ // apparmor_parser was used to load the both profiles
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), hookProfile},
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), nmbdProfile},
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), smbdProfile},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1WithNmbd, 1)
+ s.parserCmd.ForgetCalls()
+ // NOTE: the revision is kept the same to just test on the application being removed
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 1)
+ smbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ nmbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.nmbd")
+ // file called "snap.sambda.nmbd" was removed
+ _, err := os.Stat(nmbdProfile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // apparmor_parser was used to remove the unused profile
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), smbdProfile},
+ {"apparmor_parser", "--remove", "snap.samba.nmbd"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerHooks(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlWithHook, 1)
+ s.parserCmd.ForgetCalls()
+ // NOTE: the revision is kept the same to just test on the application being removed
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 1)
+ smbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ nmbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.nmbd")
+ hookProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.hook.configure")
+
+ // Verify profile "snap.samba.hook.configure" was removed
+ _, err := os.Stat(hookProfile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // apparmor_parser was used to remove the unused profile
+ c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), nmbdProfile},
+ {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), smbdProfile},
+ {"apparmor_parser", "--remove", "snap.samba.hook.configure"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestRealDefaultTemplateIsNormallyUsed(c *C) {
+ snapInfo := snaptest.MockInfo(c, ifacetest.SambaYamlV1, nil)
+ // NOTE: we don't call apparmor.MockTemplate()
+ err := s.Backend.Setup(snapInfo, interfaces.ConfinementOptions{}, s.Repo)
+ c.Assert(err, IsNil)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ for _, line := range []string{
+ // NOTE: a few randomly picked lines from the real profile. Comments
+ // and empty lines are avoided as those can be discarded in the future.
+ "#include <tunables/global>\n",
+ "/tmp/ r,\n",
+ "/sys/class/ r,\n",
+ } {
+ c.Assert(string(data), testutil.Contains, line)
+ }
+}
+
+type combineSnippetsScenario struct {
+ opts interfaces.ConfinementOptions
+ snippet string
+ content string
+}
+
+const commonPrefix = `
+@{SNAP_NAME}="samba"
+@{SNAP_REVISION}="1"
+@{INSTALL_DIR}="/snap"`
+
+var combineSnippetsScenarios = []combineSnippetsScenario{{
+ // By default apparmor is enforcing mode.
+ opts: interfaces.ConfinementOptions{},
+ content: commonPrefix + "\nprofile \"snap.samba.smbd\" (attach_disconnected) {\n\n}\n",
+}, {
+ // Snippets are injected in the space between "{" and "}"
+ opts: interfaces.ConfinementOptions{},
+ snippet: "snippet",
+ content: commonPrefix + "\nprofile \"snap.samba.smbd\" (attach_disconnected) {\nsnippet\n}\n",
+}, {
+ // DevMode switches apparmor to non-enforcing (complain) mode.
+ opts: interfaces.ConfinementOptions{DevMode: true},
+ snippet: "snippet",
+ content: commonPrefix + "\nprofile \"snap.samba.smbd\" (attach_disconnected,complain) {\nsnippet\n}\n",
+}, {
+ // JailMode switches apparmor to enforcing mode even in the presence of DevMode.
+ opts: interfaces.ConfinementOptions{DevMode: true},
+ snippet: "snippet",
+ content: commonPrefix + "\nprofile \"snap.samba.smbd\" (attach_disconnected,complain) {\nsnippet\n}\n",
+}, {
+ // Classic confinement (without jailmode) uses apparmor in complain mode by default and ignores all snippets.
+ opts: interfaces.ConfinementOptions{Classic: true},
+ snippet: "snippet",
+ content: "\n#classic" + commonPrefix + "\nprofile \"snap.samba.smbd\" (attach_disconnected,complain) {\n\n}\n",
+}, {
+ // Classic confinement in JailMode uses enforcing apparmor.
+ opts: interfaces.ConfinementOptions{Classic: true, JailMode: true},
+ snippet: "snippet",
+ content: commonPrefix + `
+profile "snap.samba.smbd" (attach_disconnected) {
+
+ # Read-only access to the core snap.
+ @{INSTALL_DIR}/core/** r,
+
+snippet
+}
+`,
+}}
+
+func (s *backendSuite) TestCombineSnippets(c *C) {
+ // NOTE: replace the real template with a shorter variant
+ restoreTemplate := apparmor.MockTemplate([]byte("\n" +
+ "###VAR###\n" +
+ "###PROFILEATTACH### (attach_disconnected) {\n" +
+ "###SNIPPETS###\n" +
+ "}\n"))
+ defer restoreTemplate()
+ restoreClassicTemplate := apparmor.MockClassicTemplate([]byte("\n" +
+ "#classic\n" +
+ "###VAR###\n" +
+ "###PROFILEATTACH### (attach_disconnected) {\n" +
+ "###SNIPPETS###\n" +
+ "}\n"))
+ defer restoreClassicTemplate()
+ for _, scenario := range combineSnippetsScenarios {
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if scenario.snippet == "" {
+ return nil, nil
+ }
+ return []byte(scenario.snippet), nil
+ }
+ snapInfo := s.InstallSnap(c, scenario.opts, ifacetest.SambaYamlV1, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ c.Check(string(data), Equals, scenario.content)
+ stat, err := os.Stat(profile)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode(), Equals, os.FileMode(0644))
+ s.RemoveSnap(c, snapInfo)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package apparmor
+
+import (
+ "github.com/snapcore/snapd/testutil"
+)
+
+// MockProfilesPath mocks the file read by LoadedProfiles()
+func MockProfilesPath(t *testutil.BaseTest, profiles string) {
+ profilesPath = profiles
+ t.AddCleanup(func() {
+ profilesPath = realProfilesPath
+ })
+}
+
+// MockTemplate replaces apprmor template.
+//
+// NOTE: The real apparmor template is long. For testing it is convenient for
+// replace it with a shorter snippet.
+func MockTemplate(fakeTemplate []byte) (restore func()) {
+ orig := defaultTemplate
+ defaultTemplate = fakeTemplate
+ return func() { defaultTemplate = orig }
+}
+
+// MockClassicTemplate replaces the classic apprmor template.
+func MockClassicTemplate(fakeTemplate []byte) (restore func()) {
+ orig := classicTemplate
+ classicTemplate = fakeTemplate
+ return func() { classicTemplate = orig }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package apparmor
+
+// defaultTemplate contains default apparmor template.
+//
+// It can be overridden for testing using MockTemplate().
+//
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/templates/ubuntu-core/16.04/default
+var defaultTemplate = []byte(`
+# Description: Allows access to app-specific directories and basic runtime
+# Usage: common
+
+# vim:syntax=apparmor
+
+#include <tunables/global>
+
+###VAR###
+
+###PROFILEATTACH### (attach_disconnected) {
+ #include <abstractions/base>
+ #include <abstractions/consoles>
+ #include <abstractions/openssl>
+
+ # While in later versions of the base abstraction, include this explicitly
+ # for series 16 and cross-distro
+ /etc/ld.so.preload r,
+
+ # for python apps/services
+ #include <abstractions/python>
+ /usr/bin/python{,2,2.[0-9]*,3,3.[0-9]*} ixr,
+
+ # explicitly deny noisy denials to read-only filesystems (see LP: #1496895
+ # for details)
+ deny /usr/lib/python3*/{,**/}__pycache__/ w,
+ deny /usr/lib/python3*/{,**/}__pycache__/**.pyc.[0-9]* w,
+ deny @{INSTALL_DIR}/@{SNAP_NAME}/**/__pycache__/ w,
+ deny @{INSTALL_DIR}/@{SNAP_NAME}/**/__pycache__/*.pyc.[0-9]* w,
+
+ # for perl apps/services
+ #include <abstractions/perl>
+ /usr/bin/perl{,5*} ixr,
+
+ # Note: the following dangerous accesses should not be allowed in most
+ # policy, but we cannot explicitly deny since other trusted interfaces might
+ # add them.
+ # Explicitly deny ptrace for now since it can be abused to break out of the
+ # seccomp sandbox. https://lkml.org/lkml/2015/3/18/823
+ #audit deny ptrace (trace),
+
+ # Explicitly deny capability mknod so apps can't create devices
+ #audit deny capability mknod,
+
+ # Explicitly deny mount, remount and umount so apps can't modify things in
+ # their namespace
+ #audit deny mount,
+ #audit deny remount,
+ #audit deny umount,
+
+ # End dangerous accesses
+
+ # Note: this potentially allows snaps to DoS other snaps via resource
+ # exhaustion but we can't sensibly mediate this today. In the future we may
+ # employ cgroup limits, AppArmor rlimit mlock rules or something else.
+ capability ipc_lock,
+
+ # for bash 'binaries' (do *not* use abstractions/bash)
+ # user-specific bash files
+ /bin/bash ixr,
+ /bin/dash ixr,
+ /etc/bash.bashrc r,
+ /etc/{passwd,group,nsswitch.conf} r, # very common
+ /etc/libnl-3/{classid,pktloc} r, # apps that use libnl
+ /var/lib/extrausers/{passwd,group} r,
+ /etc/profile r,
+ /etc/environment r,
+ /usr/share/terminfo/** r,
+ /etc/inputrc r,
+ # Common utilities for shell scripts
+ /{,usr/}bin/{,g,m}awk ixr,
+ /{,usr/}bin/basename ixr,
+ /{,usr/}bin/bunzip2 ixr,
+ /{,usr/}bin/bzcat ixr,
+ /{,usr/}bin/bzdiff ixr,
+ /{,usr/}bin/bzgrep ixr,
+ /{,usr/}bin/bzip2 ixr,
+ /{,usr/}bin/cat ixr,
+ /{,usr/}bin/chmod ixr,
+ /{,usr/}bin/clear ixr,
+ /{,usr/}bin/cmp ixr,
+ /{,usr/}bin/cp ixr,
+ /{,usr/}bin/cpio ixr,
+ /{,usr/}bin/cut ixr,
+ /{,usr/}bin/date ixr,
+ /{,usr/}bin/dd ixr,
+ /{,usr/}bin/diff{,3} ixr,
+ /{,usr/}bin/dir ixr,
+ /{,usr/}bin/dirname ixr,
+ /{,usr/}bin/echo ixr,
+ /{,usr/}bin/{,e,f,r}grep ixr,
+ /{,usr/}bin/env ixr,
+ /{,usr/}bin/expr ixr,
+ /{,usr/}bin/false ixr,
+ /{,usr/}bin/find ixr,
+ /{,usr/}bin/fmt ixr,
+ /{,usr/}bin/getopt ixr,
+ /{,usr/}bin/groups ixr,
+ /{,usr/}bin/gzip ixr,
+ /{,usr/}bin/head ixr,
+ /{,usr/}bin/hostname ixr,
+ /{,usr/}bin/id ixr,
+ /{,usr/}bin/igawk ixr,
+ /{,usr/}bin/infocmp ixr,
+ /{,usr/}bin/kill ixr,
+ /{,usr/}bin/ldd ixr,
+ /{,usr/}bin/less{,file,pipe} ixr,
+ /{,usr/}bin/ln ixr,
+ /{,usr/}bin/line ixr,
+ /{,usr/}bin/link ixr,
+ /{,usr/}bin/locale ixr,
+ /{,usr/}bin/logger ixr,
+ /{,usr/}bin/ls ixr,
+ /{,usr/}bin/md5sum ixr,
+ /{,usr/}bin/mkdir ixr,
+ /{,usr/}bin/mktemp ixr,
+ /{,usr/}bin/more ixr,
+ /{,usr/}bin/mv ixr,
+ /{,usr/}bin/openssl ixr, # may cause harmless capability block_suspend denial
+ /{,usr/}bin/pgrep ixr,
+ /{,usr/}bin/printenv ixr,
+ /{,usr/}bin/printf ixr,
+ /{,usr/}bin/ps ixr,
+ /{,usr/}bin/pwd ixr,
+ /{,usr/}bin/readlink ixr,
+ /{,usr/}bin/realpath ixr,
+ /{,usr/}bin/rev ixr,
+ /{,usr/}bin/rm ixr,
+ /{,usr/}bin/rmdir ixr,
+ /{,usr/}bin/run-parts ixr,
+ /{,usr/}bin/sed ixr,
+ /{,usr/}bin/seq ixr,
+ /{,usr/}bin/sha{1,224,256,384,512}sum ixr,
+ /{,usr/}bin/shuf ixr,
+ /{,usr/}bin/sleep ixr,
+ /{,usr/}bin/sort ixr,
+ /{,usr/}bin/stat ixr,
+ /{,usr/}bin/stdbuf ixr,
+ /{,usr/}bin/stty ixr,
+ /{,usr/}bin/tac ixr,
+ /{,usr/}bin/tail ixr,
+ /{,usr/}bin/tar ixr,
+ /{,usr/}bin/tee ixr,
+ /{,usr/}bin/test ixr,
+ /{,usr/}bin/tempfile ixr,
+ /{,usr/}bin/tset ixr,
+ /{,usr/}bin/touch ixr,
+ /{,usr/}bin/tput ixr,
+ /{,usr/}bin/tr ixr,
+ /{,usr/}bin/true ixr,
+ /{,usr/}bin/tty ixr,
+ /{,usr/}bin/uname ixr,
+ /{,usr/}bin/uniq ixr,
+ /{,usr/}bin/unlink ixr,
+ /{,usr/}bin/unxz ixr,
+ /{,usr/}bin/unzip ixr,
+ /{,usr/}bin/vdir ixr,
+ /{,usr/}bin/wc ixr,
+ /{,usr/}bin/which ixr,
+ /{,usr/}bin/xargs ixr,
+ /{,usr/}bin/xz ixr,
+ /{,usr/}bin/yes ixr,
+ /{,usr/}bin/zcat ixr,
+ /{,usr/}bin/z{,e,f}grep ixr,
+ /{,usr/}bin/zip ixr,
+ /{,usr/}bin/zipgrep ixr,
+
+ # For snappy reexec on 4.8+ kernels
+ /usr/lib/snapd/snap-exec m,
+
+ # For printing the cache (we don't allow updating the cache)
+ /{,usr/}sbin/ldconfig{,.real} ixr,
+
+ # uptime
+ /{,usr/}bin/uptime ixr,
+ @{PROC}/uptime r,
+ @{PROC}/loadavg r,
+
+ # lsb-release
+ /usr/bin/lsb_release ixr,
+ /usr/bin/ r,
+ /usr/share/distro-info/*.csv r,
+
+ # Allow reading /etc/os-release. On Ubuntu 16.04+ it is a symlink to /usr/lib
+ # but on 14.04 it is an actual file so it doens't fall under other rules.
+ /etc/os-release r,
+
+ # systemd native journal API (see sd_journal_print(4)). This should be in
+ # AppArmor's base abstraction, but until it is, include here.
+ /run/systemd/journal/socket w,
+
+ # snapctl and its requirements
+ /usr/bin/snapctl ixr,
+ @{PROC}/sys/net/core/somaxconn r,
+ /run/snapd-snap.socket rw,
+
+ # Note: for now, don't explicitly deny this noisy denial so --devmode isn't
+ # broken but eventually we may conditionally deny this since it is an
+ # information leak.
+ #deny /{,var/}run/utmp r,
+
+ # java
+ @{PROC}/@{pid}/ r,
+ @{PROC}/@{pid}/fd/ r,
+ owner @{PROC}/@{pid}/auxv r,
+ @{PROC}/sys/vm/zone_reclaim_mode r,
+ /etc/lsb-release r,
+ /sys/devices/**/read_ahead_kb r,
+ /sys/devices/system/cpu/** r,
+ /sys/devices/system/node/node[0-9]*/* r,
+ /sys/kernel/mm/transparent_hugepage/enabled r,
+ /sys/kernel/mm/transparent_hugepage/defrag r,
+ # NOTE: this leaks running process but java seems to want it (even though it
+ # seems to operate ok without it) and SDL apps crash without it. Allow owner
+ # match until AppArmor kernel var is available to solve this properly (see
+ # LP: #1546825 for details)
+ owner @{PROC}/@{pid}/cmdline r,
+ owner @{PROC}/@{pid}/comm r,
+
+ # Per man(5) proc, the kernel enforces that a thread may only modify its comm
+ # value or those in its thread group.
+ owner @{PROC}/@{pid}/task/@{tid}/comm rw,
+
+ # Miscellaneous accesses
+ /dev/{,u}random w,
+ /etc/machine-id r,
+ /etc/mime.types r,
+ @{PROC}/ r,
+ @{PROC}/version r,
+ @{PROC}/version_signature r,
+ /etc/{,writable/}hostname r,
+ /etc/{,writable/}localtime r,
+ /etc/{,writable/}timezone r,
+ @{PROC}/@{pid}/io r,
+ owner @{PROC}/@{pid}/limits r,
+ @{PROC}/@{pid}/smaps r,
+ @{PROC}/@{pid}/stat r,
+ @{PROC}/@{pid}/statm r,
+ @{PROC}/@{pid}/status r,
+ @{PROC}/@{pid}/task/ r,
+ @{PROC}/@{pid}/task/[0-9]*/smaps r,
+ @{PROC}/@{pid}/task/[0-9]*/stat r,
+ @{PROC}/@{pid}/task/[0-9]*/statm r,
+ @{PROC}/@{pid}/task/[0-9]*/status r,
+ @{PROC}/sys/kernel/hostname r,
+ @{PROC}/sys/kernel/osrelease r,
+ @{PROC}/sys/kernel/yama/ptrace_scope r,
+ @{PROC}/sys/kernel/shmmax r,
+ @{PROC}/sys/fs/file-max r,
+ @{PROC}/sys/kernel/pid_max r,
+ @{PROC}/sys/kernel/random/uuid r,
+ /sys/devices/virtual/tty/{console,tty*}/active r,
+ /{,usr/}lib/ r,
+
+ # Reads of oom_adj and oom_score_adj are safe
+ owner @{PROC}/@{pid}/oom_{,score_}adj r,
+
+ # Note: for now, don't explicitly deny write access so --devmode isn't broken
+ # but eventually we may conditionally deny this since it allows the process
+ # to increase the oom heuristic of other processes (make them more likely to
+ # be killed). Once AppArmor kernel var is available to solve this properly,
+ # this can safely be allowed since non-root processes won't be able to
+ # decrease the value and root processes will only be able to with
+ # 'capability sys_resource,' which we deny be default.
+ # deny owner @{PROC}/@{pid}/oom_{,score_}adj w,
+
+ # Eases hardware assignment (doesn't give anything away)
+ /etc/udev/udev.conf r,
+ /sys/ r,
+ /sys/bus/ r,
+ /sys/class/ r,
+
+ # this leaks interface names and stats, but not in a way that is traceable
+ # to the user/device
+ @{PROC}/net/dev r,
+ @{PROC}/@{pid}/net/dev r,
+
+ # Read-only for the install directory
+ @{INSTALL_DIR}/@{SNAP_NAME}/ r,
+ @{INSTALL_DIR}/@{SNAP_NAME}/@{SNAP_REVISION}/ r,
+ @{INSTALL_DIR}/@{SNAP_NAME}/@{SNAP_REVISION}/** mrklix,
+
+ # Read-only install directory for other revisions to help with bugs like
+ # LP: #1616650 and LP: #1655992
+ @{INSTALL_DIR}/@{SNAP_NAME}/** mrkix,
+
+ # Read-only home area for other versions
+ owner @{HOME}/snap/@{SNAP_NAME}/ r,
+ owner @{HOME}/snap/@{SNAP_NAME}/** mrkix,
+
+ # Writable home area for this version.
+ owner @{HOME}/snap/@{SNAP_NAME}/@{SNAP_REVISION}/** wl,
+ owner @{HOME}/snap/@{SNAP_NAME}/common/** wl,
+
+ # Read-only system area for other versions
+ /var/snap/@{SNAP_NAME}/ r,
+ /var/snap/@{SNAP_NAME}/** mrkix,
+
+ # Writable system area only for this version
+ /var/snap/@{SNAP_NAME}/@{SNAP_REVISION}/** wl,
+ /var/snap/@{SNAP_NAME}/common/** wl,
+
+ # The ubuntu-core-launcher creates an app-specific private restricted /tmp
+ # and will fail to launch the app if something goes wrong. As such, we can
+ # simply allow full access to /tmp.
+ /tmp/ r,
+ /tmp/** mrwlkix,
+
+ # App-specific access to files and directories in /dev/shm. We allow file
+ # access in /dev/shm for shm_open() and files in subdirectories for open()
+ /{dev,run}/shm/snap.@{SNAP_NAME}.** mrwlkix,
+ # Also allow app-specific access for sem_open()
+ /{dev,run}/shm/sem.snap.@{SNAP_NAME}.* rwk,
+
+ # Snap-specific XDG_RUNTIME_DIR that is based on the UID of the user
+ owner /{dev,run}/user/[0-9]*/snap.@{SNAP_NAME}/ rw,
+ owner /{dev,run}/user/[0-9]*/snap.@{SNAP_NAME}/** mrwklix,
+
+ # Allow apps from the same package to communicate with each other via an
+ # abstract or anonymous socket
+ unix peer=(label=snap.@{SNAP_NAME}.*),
+
+ # Allow apps from the same package to signal each other via signals
+ signal peer=snap.@{SNAP_NAME}.*,
+
+ # for 'udevadm trigger --verbose --dry-run --tag-match=snappy-assign'
+ /{,s}bin/udevadm ixr,
+ /etc/udev/udev.conf r,
+ /{,var/}run/udev/tags/snappy-assign/ r,
+ @{PROC}/cmdline r,
+ /sys/devices/**/uevent r,
+
+ # LP: #1447237: adding '--property-match=SNAPPY_APP=<pkgname>' to the above
+ # requires:
+ # /run/udev/data/* r,
+ # but that reveals too much about the system and cannot be granted to apps
+ # by default at this time.
+
+ # For convenience, allow apps to see what is in /dev even though cgroups
+ # will block most access
+ /dev/ r,
+ /dev/**/ r,
+
+ # Allow setting up pseudoterminal via /dev/pts system. This is safe because
+ # the launcher uses a per-app devpts newinstance.
+ /dev/ptmx rw,
+
+ # Do the same with /sys/devices and /sys/class to help people using hw-assign
+ /sys/devices/ r,
+ /sys/devices/**/ r,
+ /sys/class/ r,
+ /sys/class/**/ r,
+
+###SNIPPETS###
+}
+`)
+
+// classicTemplate contains apparmor template used for snaps with classic
+// confinement. This template was Designed by jdstrand:
+// https://github.com/snapcore/snapd/pull/2366#discussion_r90101320
+//
+// The classic template intentionally provides no confinement and is used
+// simply to ensure that processes have the proper command-specific security
+// label instead of 'unconfined'.
+//
+// It can be overridden for testing using MockClassicTemplate().
+var classicTemplate = []byte(`
+#include <tunables/global>
+
+###VAR###
+
+###PROFILEATTACH### (attach_disconnected) {
+ # set file rules so that exec() inherits our profile unless there is
+ # already a profile for it (eg, snap-confine)
+ / rwkl,
+ /** rwlkm,
+ /** pix,
+
+ capability,
+ change_profile,
+ dbus,
+ network,
+ mount,
+ remount,
+ umount,
+ pivot_root,
+ ptrace,
+ signal,
+ unix,
+
+###SNIPPETS###
+}
+`)
+
+// classicJailmodeSnippet contains extra rules that allow snaps using classic
+// confinement, that were put in to jailmode, to execute by at least having
+// access to the core snap (e.g. for the dynamic linker and libc).
+
+var classicJailmodeSnippet = []byte(`
+ # Read-only access to the core snap.
+ @{INSTALL_DIR}/core/** r,
+`)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package apparmor
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// templateVariables returns text defining apparmor variables that can be used in the
+// apparmor template and by apparmor snippets.
+func templateVariables(info *snap.Info) []byte {
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, "@{SNAP_NAME}=\"%s\"\n", info.Name())
+ fmt.Fprintf(&buf, "@{SNAP_REVISION}=\"%s\"\n", info.Revision)
+ fmt.Fprintf(&buf, "@{INSTALL_DIR}=\"/snap\"")
+ return buf.Bytes()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+import (
+ "github.com/snapcore/snapd/snap"
+)
+
+// ConfinementOptions describe confinement configuration.
+//
+// The confinement system controls the initial layout of the mount namespace as
+// well as the set of actions a process is allowed to perform. Confinement is
+// initially defined by the ConfinementType declared by the snap. It can be
+// either "strict", "devmode" or "classic".
+//
+// The "strict" type uses mount layout that puts the core snap as the root
+// filesystem and provides strong isolation from the system and from other
+// snaps. Violations cause permission errors or mandatory process termination.
+//
+// The "devmode" type uses the same mount layout as "strict" but switches
+// confinement to non-enforcing mode whenever possible. Violations that would
+// result in permission error or process termination are instead permitted. A
+// diagnostic message is logged when this occurs.
+//
+// The "classic" type uses mount layout that is identical to the runtime of the
+// classic system snapd runs in, in other words there is no "chroot". Most of
+// the confinement is lifted, specifically there's no seccomp filter being
+// applied and apparmor is using complain mode by default.
+//
+// The three types defined above map to some combinations of the three flags
+// defined below.
+//
+// The DevMode flag attempts to switch all confinement facilities into
+// non-enforcing mode even if the snap requested otherwise.
+//
+// The JailMode flag attempts to switch all confinement facilities into
+// enforcing mode even if the snap requested otherwise.
+//
+// The Classic flag switches the layout of the mount namespace so that there's
+// no "chroot" to the core snap.
+type ConfinementOptions struct {
+ // DevMode flag switches confinement to non-enforcing mode.
+ DevMode bool
+ // JailMode flag switches confinement to enforcing mode.
+ JailMode bool
+ // Classic flag switches the core snap "chroot" off.
+ Classic bool
+}
+
+// SecurityBackend abstracts interactions between the interface system and the
+// needs of a particular security system.
+type SecurityBackend interface {
+ // Name returns the name of the backend.
+ // This is intended for diagnostic messages.
+ Name() string
+
+ // Setup creates and loads security artefacts specific to a given snap.
+ // The snap can be in one of three kids onf confinement (strict mode,
+ // developer mode or classic mode). In the last two security violations
+ // are non-fatal to the offending application process.
+ //
+ // This method should be called after changing plug, slots, connections
+ // between them or application present in the snap.
+ Setup(snapInfo *snap.Info, opts ConfinementOptions, repo *Repository) error
+
+ // Remove removes and unloads security artefacts of a given snap.
+ //
+ // This method should be called during the process of removing a snap.
+ Remove(snapName string) error
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backends
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/dbus"
+ "github.com/snapcore/snapd/interfaces/kmod"
+ "github.com/snapcore/snapd/interfaces/mount"
+ "github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/interfaces/systemd"
+ "github.com/snapcore/snapd/interfaces/udev"
+ "github.com/snapcore/snapd/release"
+)
+
+// append when a new security backend is added
+var All = []interfaces.SecurityBackend{
+ &systemd.Backend{},
+ &seccomp.Backend{},
+ &dbus.Backend{},
+ &udev.Backend{},
+ &mount.Backend{},
+ &kmod.Backend{},
+}
+
+func init() {
+ if !release.ReleaseInfo.ForceDevMode() {
+ All = append(All, &apparmor.Backend{})
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var allInterfaces = []interfaces.Interface{
+ &BluezInterface{},
+ &BoolFileInterface{},
+ &BrowserSupportInterface{},
+ &ContentInterface{},
+ &DbusInterface{},
+ &DockerInterface{},
+ &DockerSupportInterface{},
+ &FwupdInterface{},
+ &GpioInterface{},
+ &HidrawInterface{},
+ &I2cInterface{},
+ &IioInterface{},
+ &IioPortsControlInterface{},
+ &LocationControlInterface{},
+ &LocationObserveInterface{},
+ &LxdInterface{},
+ &LxdSupportInterface{},
+ &MirInterface{},
+ &ModemManagerInterface{},
+ &MprisInterface{},
+ &NetworkManagerInterface{},
+ &OfonoInterface{},
+ &PhysicalMemoryControlInterface{},
+ &PhysicalMemoryObserveInterface{},
+ &PppInterface{},
+ &PulseAudioInterface{},
+ &SerialPortInterface{},
+ &TimeControlInterface{},
+ &UDisks2Interface{},
+ &UpowerObserveInterface{},
+ NewAlsaInterface(),
+ NewAvahiObserveInterface(),
+ NewBluetoothControlInterface(),
+ NewCameraInterface(),
+ NewCupsControlInterface(),
+ NewDcdbasControlInterface(),
+ NewFirewallControlInterface(),
+ NewFuseSupportInterface(),
+ NewGsettingsInterface(),
+ NewHardwareObserveInterface(),
+ NewHomeInterface(),
+ NewKernelModuleControlInterface(),
+ NewLibvirtInterface(),
+ NewLocaleControlInterface(),
+ NewLogObserveInterface(),
+ NewMountObserveInterface(),
+ NewNetworkBindInterface(),
+ NewNetworkControlInterface(),
+ NewNetworkInterface(),
+ NewNetworkObserveInterface(),
+ NewNetworkSetupObserveInterface(),
+ NewOpenglInterface(),
+ NewOpenvSwitchInterface(),
+ NewOpenvSwitchSupportInterface(),
+ NewOpticalDriveInterface(),
+ NewProcessControlInterface(),
+ NewRawUsbInterface(),
+ NewRemovableMediaInterface(),
+ NewScreenInhibitControlInterface(),
+ NewShutdownInterface(),
+ NewSnapdControlInterface(),
+ NewSystemObserveInterface(),
+ NewSystemTraceInterface(),
+ NewTimeserverControlInterface(),
+ NewTimezoneControlInterface(),
+ NewTpmInterface(),
+ NewUnity7Interface(),
+ NewX11Interface(),
+}
+
+// Interfaces returns all of the built-in interfaces.
+func Interfaces() []interfaces.Interface {
+ return allInterfaces
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ "github.com/snapcore/snapd/interfaces/builtin"
+ . "github.com/snapcore/snapd/testutil"
+
+ . "gopkg.in/check.v1"
+)
+
+type AllSuite struct{}
+
+var _ = Suite(&AllSuite{})
+
+func (s *AllSuite) TestInterfaces(c *C) {
+ all := builtin.Interfaces()
+ c.Check(all, Contains, &builtin.BluezInterface{})
+ c.Check(all, Contains, &builtin.BoolFileInterface{})
+ c.Check(all, Contains, &builtin.BrowserSupportInterface{})
+ c.Check(all, Contains, &builtin.DbusInterface{})
+ c.Check(all, Contains, &builtin.DockerInterface{})
+ c.Check(all, Contains, &builtin.DockerSupportInterface{})
+ c.Check(all, Contains, &builtin.FwupdInterface{})
+ c.Check(all, Contains, &builtin.FwupdInterface{})
+ c.Check(all, Contains, &builtin.GpioInterface{})
+ c.Check(all, Contains, &builtin.HidrawInterface{})
+ c.Check(all, Contains, &builtin.I2cInterface{})
+ c.Check(all, Contains, &builtin.IioInterface{})
+ c.Check(all, Contains, &builtin.IioPortsControlInterface{})
+ c.Check(all, Contains, &builtin.LocationControlInterface{})
+ c.Check(all, Contains, &builtin.LocationObserveInterface{})
+ c.Check(all, Contains, &builtin.LxdSupportInterface{})
+ c.Check(all, Contains, &builtin.MirInterface{})
+ c.Check(all, Contains, &builtin.MprisInterface{})
+ c.Check(all, Contains, &builtin.PhysicalMemoryControlInterface{})
+ c.Check(all, Contains, &builtin.PhysicalMemoryObserveInterface{})
+ c.Check(all, Contains, &builtin.PulseAudioInterface{})
+ c.Check(all, Contains, &builtin.SerialPortInterface{})
+ c.Check(all, Contains, &builtin.TimeControlInterface{})
+ c.Check(all, Contains, &builtin.UDisks2Interface{})
+ c.Check(all, Contains, &builtin.UpowerObserveInterface{})
+ c.Check(all, DeepContains, builtin.NewAlsaInterface())
+ c.Check(all, DeepContains, builtin.NewAvahiObserveInterface())
+ c.Check(all, DeepContains, builtin.NewBluetoothControlInterface())
+ c.Check(all, DeepContains, builtin.NewCameraInterface())
+ c.Check(all, DeepContains, builtin.NewCupsControlInterface())
+ c.Check(all, DeepContains, builtin.NewFirewallControlInterface())
+ c.Check(all, DeepContains, builtin.NewFuseSupportInterface())
+ c.Check(all, DeepContains, builtin.NewGsettingsInterface())
+ c.Check(all, DeepContains, builtin.NewHomeInterface())
+ c.Check(all, DeepContains, builtin.NewKernelModuleControlInterface())
+ c.Check(all, DeepContains, builtin.NewLocaleControlInterface())
+ c.Check(all, DeepContains, builtin.NewLogObserveInterface())
+ c.Check(all, DeepContains, builtin.NewMountObserveInterface())
+ c.Check(all, DeepContains, builtin.NewNetworkBindInterface())
+ c.Check(all, DeepContains, builtin.NewNetworkControlInterface())
+ c.Check(all, DeepContains, builtin.NewNetworkInterface())
+ c.Check(all, DeepContains, builtin.NewNetworkObserveInterface())
+ c.Check(all, DeepContains, builtin.NewOpenglInterface())
+ c.Check(all, DeepContains, builtin.NewOpenvSwitchInterface())
+ c.Check(all, DeepContains, builtin.NewOpenvSwitchSupportInterface())
+ c.Check(all, DeepContains, builtin.NewOpticalDriveInterface())
+ c.Check(all, DeepContains, builtin.NewProcessControlInterface())
+ c.Check(all, DeepContains, builtin.NewRawUsbInterface())
+ c.Check(all, DeepContains, builtin.NewRemovableMediaInterface())
+ c.Check(all, DeepContains, builtin.NewScreenInhibitControlInterface())
+ c.Check(all, DeepContains, builtin.NewSnapdControlInterface())
+ c.Check(all, DeepContains, builtin.NewSystemObserveInterface())
+ c.Check(all, DeepContains, builtin.NewSystemTraceInterface())
+ c.Check(all, DeepContains, builtin.NewTimeserverControlInterface())
+ c.Check(all, DeepContains, builtin.NewTimezoneControlInterface())
+ c.Check(all, DeepContains, builtin.NewTpmInterface())
+ c.Check(all, DeepContains, builtin.NewUnity7Interface())
+ c.Check(all, DeepContains, builtin.NewX11Interface())
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const alsaConnectedPlugAppArmor = `
+# Description: Allow access to raw ALSA devices.
+
+/dev/snd/ r,
+/dev/snd/* rw,
+
+/run/udev/data/c116:[0-9]* r, # alsa
+`
+
+func NewAlsaInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "alsa",
+ connectedPlugAppArmor: alsaConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type AlsaInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&AlsaInterfaceSuite{
+ iface: builtin.NewAlsaInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "alsa",
+ Interface: "alsa",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "alsa",
+ Interface: "alsa",
+ },
+ },
+})
+
+func (s *AlsaInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "alsa")
+}
+
+func (s *AlsaInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "alsa",
+ Interface: "alsa",
+ }})
+ c.Assert(err, ErrorMatches, "alsa slots are reserved for the operating system snap")
+}
+
+func (s *AlsaInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *AlsaInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "alsa"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "alsa"`)
+}
+
+func (s *AlsaInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "/dev/snd/* rw,")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const avahiObserveConnectedPlugAppArmor = `
+# Description: allows domain browsing, service browsing and service resolving
+
+#include <abstractions/dbus-strict>
+dbus (send)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.Peer
+ member=Ping
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/
+ interface=org.freedesktop.Avahi.Server
+ member=Get*
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+
+# These allows tampering with other snap's browsers, so don't autoconnect for
+# now.
+
+# service browsing
+dbus (send)
+ bus=system
+ path=/
+ interface=org.freedesktop.Avahi.Server
+ member=ServiceBrowserNew
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/Client*/ServiceBrowser*
+ interface=org.freedesktop.Avahi.ServiceBrowser
+ member=Free
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (receive)
+ bus=system
+ interface=org.freedesktop.Avahi.ServiceBrowser
+ peer=(label=unconfined),
+
+# service resolving
+dbus (send)
+ bus=system
+ path=/
+ interface=org.freedesktop.Avahi.Server
+ member=ServiceResolverNew
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/Client*/ServiceResolver*
+ interface=org.freedesktop.Avahi.ServiceResolver
+ member=Free
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (receive)
+ bus=system
+ interface=org.freedesktop.Avahi.ServiceResolver
+ peer=(label=unconfined),
+
+# domain browsing
+dbus (send)
+ bus=system
+ path=/
+ interface=org.freedesktop.Avahi.Server
+ member=DomainBrowserNew
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/Client*/DomainBrowser*
+ interface=org.freedesktop.Avahi.DomainBrowser
+ member=Free
+ peer=(name=org.freedesktop.Avahi,label=unconfined),
+
+dbus (receive)
+ bus=system
+ path=/Client*/DomainBrowser*
+ interface=org.freedesktop.Avahi.DomainBrowser
+ peer=(label=unconfined),
+`
+
+const avahiObserveConnectedPlugSecComp = `
+# Description: allows domain browsing, service browsing and service resolving
+
+# dbus
+connect
+getsockname
+recvfrom
+recvmsg
+send
+sendto
+sendmsg
+socket
+`
+
+func NewAvahiObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "avahi-observe",
+ connectedPlugAppArmor: avahiObserveConnectedPlugAppArmor,
+ connectedPlugSecComp: avahiObserveConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type AvahiObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&AvahiObserveInterfaceSuite{
+ iface: builtin.NewAvahiObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "avahi-observe",
+ Interface: "avahi-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "avahi-observe",
+ Interface: "avahi-observe",
+ },
+ },
+})
+
+func (s *AvahiObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "avahi-observe")
+}
+
+func (s *AvahiObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "avahi-observe",
+ Interface: "avahi-observe",
+ }})
+ c.Assert(err, ErrorMatches, "avahi-observe slots are reserved for the operating system snap")
+}
+
+func (s *AvahiObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *AvahiObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "avahi-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "avahi-observe"`)
+}
+
+func (s *AvahiObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "name=org.freedesktop.Avahi")
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "sendto")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+// The headers of the builtin base-declaration describing the default
+// interface policies for all snaps. The base declaration focuses on the slot
+// side for almost all interfaces. Importantly, items are not merged between
+// the slots and plugs or between the base declaration and snap declaration
+// for a particular type of rule. This means that if you specify an
+// installation rule for both slots and plugs in the base declaration, only
+// the plugs side is used (plugs is preferred over slots).
+//
+// The interfaces listed in the base declaration can be broadly categorized
+// into:
+//
+// - manually connected implicit slots (eg, bluetooth-control)
+// - auto-connected implicit slots (eg, network)
+// - manually connected app-provided slots (eg, bluez)
+// - auto-connected app-provided slots (eg, mir)
+//
+// such that they will follow this pattern:
+//
+// slots:
+// manual-connected-implicit-slot:
+// allow-installation:
+// slot-snap-type:
+// - core # implicit slot
+// deny-auto-connection: true # force manual connect
+//
+// auto-connected-implicit-slot:
+// allow-installation:
+// slot-snap-type:
+// - core # implicit slot
+// allow-auto-connection: true # allow auto-connect
+//
+// manual-connected-provided-slot:
+// allow-installation:
+// slot-snap-type:
+// - app # app provided slot
+// deny-connection: true # require allow-connection in snap decl
+// deny-auto-connection: true # force manual connect
+//
+// auto-connected-provided-slot:
+// allow-installation:
+// slot-snap-type:
+// - app # app provided slot
+// deny-connection: true # require allow-connection in snap decl
+//
+// App-provided slots use 'deny-connection: true' since slot implementations
+// require privileged access to the system and the snap must be trusted. In
+// this manner a snap declaration is required to override the base declaration
+// to allow connections with the app-provided slot.
+//
+// Slots dealing with hardware typically will specify 'gadget' and 'core' as
+// the slot-snap-type (eg, serial-port). Eg:
+//
+// slots:
+// manual-connected-hw-slot:
+// allow-installation:
+// slot-snap-type:
+// - core
+// - gadget
+// deny-auto-connection: true
+//
+// So called super-privileged slot implementations should also be disallowed
+// installation on a system and a snap declaration is required to override the
+// base declaration to allow installation (eg, docker). Eg:
+//
+// slots:
+// manual-connected-super-privileged-slot:
+// allow-installation: false
+// deny-connection: true
+// deny-auto-connection: true
+//
+// Like super-privileged slot implementation, super-privileged plugs should
+// also be disallowed installation on a system and a snap declaration is
+// required to override the base declaration to allow installation (eg,
+// kernel-module-control). Eg:
+//
+// plugs:
+// manual-connected-super-privileged-plug:
+// allow-installation: false
+// deny-auto-connection: true
+// (remember this overrides slot side rules)
+//
+// Some interfaces have policy that is meant to be used with slot
+// implementations and on classic images. Since the slot implementation is
+// privileged, we require a snap declaration to be used for app-provided slot
+// implementations on non-classic systems (eg, network-manager). Eg:
+//
+// slots:
+// classic-or-not-slot:
+// allow-installation:
+// slot-snap-type:
+// - app
+// - core
+// deny-auto-connection: true
+// deny-connection:
+// on-classic: false
+//
+// Some interfaces have policy that is only used with implicit slots on
+// classic and should be autoconnected only there (eg, home). Eg:
+//
+// slots:
+// implicit-classic-slot:
+// allow-installation:
+// slot-snap-type:
+// - core
+// deny-auto-connection:
+// on-classic: false
+//
+const baseDeclarationHeaders = `
+type: base-declaration
+authority-id: canonical
+series: 16
+revision: 0
+plugs:
+ docker-support:
+ allow-installation: false
+ deny-auto-connection: true
+ kernel-module-control:
+ allow-installation: false
+ deny-auto-connection: true
+ lxd-support:
+ allow-installation: false
+ deny-auto-connection: true
+ snapd-control:
+ allow-installation: false
+ deny-auto-connection: true
+slots:
+ alsa:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ avahi-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ bluetooth-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ bluez:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
+ deny-auto-connection: true
+ bool-file:
+ allow-installation:
+ slot-snap-type:
+ - core
+ - gadget
+ deny-auto-connection: true
+ browser-support:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-connection:
+ plug-attributes:
+ allow-sandbox: true
+ camera:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ content:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - gadget
+ allow-auto-connection:
+ plug-publisher-id:
+ - $SLOT_PUBLISHER_ID
+ cups-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ dbus:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection:
+ slot-attributes:
+ name: .+
+ deny-auto-connection: true
+ dcdbas-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ docker:
+ allow-installation: false
+ deny-connection: true
+ deny-auto-connection: true
+ docker-support:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ firewall-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ fuse-support:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ fwupd:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
+ deny-auto-connection: true
+ gpio:
+ allow-installation:
+ slot-snap-type:
+ - core
+ - gadget
+ deny-auto-connection: true
+ gsettings:
+ allow-installation:
+ slot-snap-type:
+ - core
+ hardware-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ hidraw:
+ allow-installation:
+ slot-snap-type:
+ - core
+ - gadget
+ deny-auto-connection: true
+ home:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection:
+ on-classic: false
+ i2c:
+ allow-installation:
+ slot-snap-type:
+ - gadget
+ - core
+ deny-auto-connection: true
+ iio:
+ allow-installation:
+ slot-snap-type:
+ - gadget
+ - core
+ deny-auto-connection: true
+ io-ports-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ kernel-module-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ libvirt:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ locale-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ location-control:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
+ deny-auto-connection: true
+ location-observe:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
+ deny-auto-connection: true
+ log-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ lxd:
+ allow-installation: false
+ deny-connection: true
+ deny-auto-connection: true
+ lxd-support:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ mir:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
+ modem-manager:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - core
+ deny-auto-connection: true
+ deny-connection:
+ on-classic: false
+ mount-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ mpris:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection:
+ slot-attributes:
+ name: .+
+ deny-auto-connection: true
+ network:
+ allow-installation:
+ slot-snap-type:
+ - core
+ network-bind:
+ allow-installation:
+ slot-snap-type:
+ - core
+ network-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ network-manager:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - core
+ deny-auto-connection: true
+ deny-connection:
+ on-classic: false
+ network-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ network-setup-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ ofono:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - core
+ deny-auto-connection: true
+ deny-connection:
+ on-classic: false
+ opengl:
+ allow-installation:
+ slot-snap-type:
+ - core
+ openvswitch:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ openvswitch-support:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ optical-drive:
+ allow-installation:
+ slot-snap-type:
+ - core
+ physical-memory-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ physical-memory-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ ppp:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ process-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ pulseaudio:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - core
+ deny-connection:
+ on-classic: false
+ raw-usb:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ removable-media:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ screen-inhibit-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ serial-port:
+ allow-installation:
+ slot-snap-type:
+ - core
+ - gadget
+ deny-auto-connection: true
+ shutdown:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ snapd-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ system-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ system-trace:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ time-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ timeserver-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ timezone-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ tpm:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+ udisks2:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
+ deny-auto-connection: true
+ unity7:
+ allow-installation:
+ slot-snap-type:
+ - core
+ upower-observe:
+ allow-installation:
+ slot-snap-type:
+ - core
+ - app
+ deny-connection:
+ on-classic: false
+ x11:
+ allow-installation:
+ slot-snap-type:
+ - core
+`
+
+func init() {
+ err := asserts.InitBuiltinBaseDeclaration([]byte(baseDeclarationHeaders))
+ if err != nil {
+ panic(fmt.Sprintf("cannot initialize the builtin base-declaration: %v", err))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ "fmt"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/policy"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type baseDeclSuite struct {
+ baseDecl *asserts.BaseDeclaration
+}
+
+var _ = Suite(&baseDeclSuite{})
+
+func (s *baseDeclSuite) SetUpSuite(c *C) {
+ s.baseDecl = asserts.BuiltinBaseDeclaration()
+}
+
+func (s *baseDeclSuite) connectCand(c *C, iface, slotYaml, plugYaml string) *policy.ConnectCandidate {
+ if slotYaml == "" {
+ slotYaml = fmt.Sprintf(`name: slot-snap
+slots:
+ %s:
+`, iface)
+ }
+ if plugYaml == "" {
+ plugYaml = fmt.Sprintf(`name: plug-snap
+plugs:
+ %s:
+`, iface)
+ }
+ slotSnap := snaptest.MockInfo(c, slotYaml, nil)
+ plugSnap := snaptest.MockInfo(c, plugYaml, nil)
+ return &policy.ConnectCandidate{
+ Plug: plugSnap.Plugs[iface],
+ Slot: slotSnap.Slots[iface],
+ BaseDeclaration: s.baseDecl,
+ }
+}
+
+func (s *baseDeclSuite) installSlotCand(c *C, iface string, snapType snap.Type, yaml string) *policy.InstallCandidate {
+ if yaml == "" {
+ yaml = fmt.Sprintf(`name: install-slot-snap
+type: %s
+slots:
+ %s:
+`, snapType, iface)
+ }
+ snap := snaptest.MockInfo(c, yaml, nil)
+ return &policy.InstallCandidate{
+ Snap: snap,
+ BaseDeclaration: s.baseDecl,
+ }
+}
+
+func (s *baseDeclSuite) installPlugCand(c *C, iface string, snapType snap.Type, yaml string) *policy.InstallCandidate {
+ if yaml == "" {
+ yaml = fmt.Sprintf(`name: install-plug-snap
+type: %s
+plugs:
+ %s:
+`, snapType, iface)
+ }
+ snap := snaptest.MockInfo(c, yaml, nil)
+ return &policy.InstallCandidate{
+ Snap: snap,
+ BaseDeclaration: s.baseDecl,
+ }
+}
+
+const declTempl = `type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: @name@
+snap-id: @snapid@
+publisher-id: @publisher@
+@plugsSlots@
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`
+
+func (s *baseDeclSuite) mockSnapDecl(c *C, name, snapID, publisher string, plugsSlots string) *asserts.SnapDeclaration {
+ encoded := strings.Replace(declTempl, "@name@", name, 1)
+ encoded = strings.Replace(encoded, "@snapid@", snapID, 1)
+ encoded = strings.Replace(encoded, "@publisher@", publisher, 1)
+ if plugsSlots != "" {
+ encoded = strings.Replace(encoded, "@plugsSlots@", strings.TrimSpace(plugsSlots), 1)
+ } else {
+ encoded = strings.Replace(encoded, "@plugsSlots@\n", "", 1)
+ }
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ return a.(*asserts.SnapDeclaration)
+}
+
+func (s *baseDeclSuite) TestAutoConnection(c *C) {
+ all := builtin.Interfaces()
+
+ // these have more complex or in flux policies and have their
+ // own separate tests
+ snowflakes := map[string]bool{
+ "content": true,
+ "home": true,
+ "lxd-support": true,
+ "snapd-control": true,
+ }
+
+ // these simply auto-connect, anything else doesn't
+ autoconnect := map[string]bool{
+ "browser-support": true,
+ "gsettings": true,
+ "mir": true,
+ "network": true,
+ "network-bind": true,
+ "opengl": true,
+ "optical-drive": true,
+ "pulseaudio": true,
+ "screen-inhibit-control": true,
+ "unity7": true,
+ "upower-observe": true,
+ "x11": true,
+ }
+
+ for _, iface := range all {
+ if snowflakes[iface.Name()] {
+ continue
+ }
+ expected := autoconnect[iface.Name()]
+ comm := Commentf(iface.Name())
+
+ // check base declaration
+ cand := s.connectCand(c, iface.Name(), "", "")
+ err := cand.CheckAutoConnect()
+ if expected {
+ c.Check(err, IsNil, comm)
+ } else {
+ c.Check(err, NotNil, comm)
+ }
+ }
+}
+
+func (s *baseDeclSuite) TestAutoConnectPlugSlot(c *C) {
+ all := builtin.Interfaces()
+
+ // these have more complex or in flux policies and have their
+ // own separate tests
+ snowflakes := map[string]bool{
+ "content": true,
+ "home": true,
+ "lxd-support": true,
+ }
+
+ for _, iface := range all {
+ if snowflakes[iface.Name()] {
+ continue
+ }
+ c.Check(iface.AutoConnect(nil, nil), Equals, true)
+ }
+}
+
+func (s *baseDeclSuite) TestInterimAutoConnectionHome(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+ cand := s.connectCand(c, "home", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+
+ release.OnClassic = false
+ err = cand.CheckAutoConnect()
+ c.Check(err, ErrorMatches, `auto-connection denied by slot rule of interface \"home\"`)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionSnapdControl(c *C) {
+ cand := s.connectCand(c, "snapd-control", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+ c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"snapd-control\"")
+
+ plugsSlots := `
+plugs:
+ snapd-control:
+ allow-auto-connection: true
+`
+
+ lxdDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = lxdDecl
+ err = cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionContent(c *C) {
+ // content will also depend for now AutoConnect(plug, slot)
+ // random snaps cannot connect with content
+ cand := s.connectCand(c, "content", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionLxdSupportOverride(c *C) {
+ // by default, don't auto-connect
+ cand := s.connectCand(c, "lxd-support", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+
+ plugsSlots := `
+plugs:
+ lxd-support:
+ allow-auto-connection: true
+`
+
+ lxdDecl := s.mockSnapDecl(c, "lxd", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = lxdDecl
+ err = cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionLxdSupportOverrideRevoke(c *C) {
+ cand := s.connectCand(c, "lxd-support", "", "")
+ plugsSlots := `
+plugs:
+ lxd-support:
+ allow-auto-connection: false
+`
+
+ lxdDecl := s.mockSnapDecl(c, "notlxd", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = lxdDecl
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+ c.Assert(err, ErrorMatches, "auto-connection not allowed by plug rule of interface \"lxd-support\" for \"notlxd\" snap")
+}
+
+func (s *baseDeclSuite) TestAutoConnectionKernelModuleControlOverride(c *C) {
+ cand := s.connectCand(c, "kernel-module-control", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+ c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"kernel-module-control\"")
+
+ plugsSlots := `
+plugs:
+ kernel-module-control:
+ allow-auto-connection: true
+`
+
+ snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = snapDecl
+ err = cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionDockerSupportOverride(c *C) {
+ cand := s.connectCand(c, "docker-support", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+ c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"docker-support\"")
+
+ plugsSlots := `
+plugs:
+ docker-support:
+ allow-auto-connection: true
+`
+
+ snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = snapDecl
+ err = cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionOverrideMultiple(c *C) {
+ plugsSlots := `
+plugs:
+ network-bind:
+ allow-auto-connection: true
+ network-control:
+ allow-auto-connection: true
+ kernel-module-control:
+ allow-auto-connection: true
+ system-observe:
+ allow-auto-connection: true
+ hardware-observe:
+ allow-auto-connection: true
+`
+
+ snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+
+ all := builtin.Interfaces()
+ // these are a mixture interfaces that the snap plugs
+ plugged := map[string]bool{
+ "network-bind": true,
+ "network-control": true,
+ "kernel-module-control": true,
+ "system-observe": true,
+ "hardware-observe": true,
+ }
+ for _, iface := range all {
+ if !plugged[iface.Name()] {
+ continue
+ }
+
+ cand := s.connectCand(c, iface.Name(), "", "")
+ cand.PlugSnapDeclaration = snapDecl
+ err := cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+ }
+}
+
+// describe installation rules for slots succinctly for cross-checking,
+// if an interface is not mentioned here a slot of its type can only
+// be installed by a core snap (and this was taken care by
+// SanitizeSlot),
+// otherwise the entry for the interface is the list of snap types it
+// can be installed by (using the declaration naming);
+// ATM a nil entry means even stricter rules that would need be tested
+// separately and whose implementation is in flux for now
+var (
+ unconstrained = []string{"core", "kernel", "gadget", "app"}
+
+ slotInstallation = map[string][]string{
+ // other
+ "bluez": {"app"},
+ "bool-file": {"core", "gadget"},
+ "browser-support": {"core"},
+ "content": {"app", "gadget"},
+ "dbus": {"app"},
+ "docker-support": {"core"},
+ "fwupd": {"app"},
+ "gpio": {"core", "gadget"},
+ "hidraw": {"core", "gadget"},
+ "i2c": {"core", "gadget"},
+ "iio": {"core", "gadget"},
+ "location-control": {"app"},
+ "location-observe": {"app"},
+ "lxd-support": {"core"},
+ "mir": {"app"},
+ "modem-manager": {"app", "core"},
+ "mpris": {"app"},
+ "network-manager": {"app", "core"},
+ "ofono": {"app", "core"},
+ "ppp": {"core"},
+ "pulseaudio": {"app", "core"},
+ "serial-port": {"core", "gadget"},
+ "udisks2": {"app"},
+ "upower-observe": {"app", "core"},
+ // snowflakes
+ "docker": nil,
+ "lxd": nil,
+ }
+)
+
+func contains(l []string, s string) bool {
+ for _, s1 := range l {
+ if s == s1 {
+ return true
+ }
+ }
+ return false
+}
+
+func (s *baseDeclSuite) TestSlotInstallation(c *C) {
+ typMap := map[string]snap.Type{
+ "core": snap.TypeOS,
+ "app": snap.TypeApp,
+ "kernel": snap.TypeKernel,
+ "gadget": snap.TypeGadget,
+ }
+
+ all := builtin.Interfaces()
+
+ for _, iface := range all {
+ types, ok := slotInstallation[iface.Name()]
+ compareWithSanitize := false
+ if !ok { // common ones, only core can install them,
+ // their plain SanitizeSlot checked for that
+ types = []string{"core"}
+ compareWithSanitize = true
+ }
+ if types == nil {
+ // snowflake needs to be tested specially
+ continue
+ }
+ for name, snapType := range typMap {
+ ok := contains(types, name)
+ ic := s.installSlotCand(c, iface.Name(), snapType, ``)
+ slotInfo := ic.Snap.Slots[iface.Name()]
+ err := ic.Check()
+ comm := Commentf("%s by %s snap", iface.Name(), name)
+ if ok {
+ c.Check(err, IsNil, comm)
+ } else {
+ c.Check(err, NotNil, comm)
+ }
+ if compareWithSanitize {
+ sanitizeErr := iface.SanitizeSlot(&interfaces.Slot{SlotInfo: slotInfo})
+ if err == nil {
+ c.Check(sanitizeErr, IsNil, comm)
+ } else {
+ c.Check(sanitizeErr, NotNil, comm)
+ }
+ }
+ }
+ }
+
+ // test docker specially
+ ic := s.installSlotCand(c, "docker", snap.TypeApp, ``)
+ err := ic.Check()
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "installation not allowed by \"docker\" slot rule of interface \"docker\"")
+
+ // test lxd specially
+ ic = s.installSlotCand(c, "lxd", snap.TypeApp, ``)
+ err = ic.Check()
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "installation not allowed by \"lxd\" slot rule of interface \"lxd\"")
+}
+
+func (s *baseDeclSuite) TestPlugInstallation(c *C) {
+ all := builtin.Interfaces()
+
+ restricted := map[string]bool{
+ "docker-support": true,
+ "kernel-module-control": true,
+ "lxd-support": true,
+ "snapd-control": true,
+ }
+
+ for _, iface := range all {
+ ic := s.installPlugCand(c, iface.Name(), snap.TypeApp, ``)
+ err := ic.Check()
+ comm := Commentf("%s", iface.Name())
+ if restricted[iface.Name()] {
+ c.Check(err, NotNil, comm)
+ } else {
+ c.Check(err, IsNil, comm)
+ }
+ }
+}
+
+func (s *baseDeclSuite) TestConnection(c *C) {
+ all := builtin.Interfaces()
+
+ // connecting with these interfaces needs to be allowed on
+ // case-by-case basis
+ noconnect := map[string]bool{
+ "bluez": true,
+ "docker": true,
+ "fwupd": true,
+ "location-control": true,
+ "location-observe": true,
+ "lxd": true,
+ "mir": true,
+ "udisks2": true,
+ }
+
+ for _, iface := range all {
+ expected := !noconnect[iface.Name()]
+ comm := Commentf(iface.Name())
+
+ // check base declaration
+ cand := s.connectCand(c, iface.Name(), "", "")
+ err := cand.Check()
+
+ if expected {
+ c.Check(err, IsNil, comm)
+ } else {
+ c.Check(err, NotNil, comm)
+ }
+ }
+}
+
+func (s *baseDeclSuite) TestConnectionOnClassic(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ all := builtin.Interfaces()
+
+ // connecting with these interfaces needs to be allowed on
+ // case-by-case basis when not on classic
+ noconnect := map[string]bool{
+ "modem-manager": true,
+ "network-manager": true,
+ "ofono": true,
+ "pulseaudio": true,
+ "upower-observe": true,
+ }
+
+ for _, onClassic := range []bool{true, false} {
+ release.OnClassic = onClassic
+ for _, iface := range all {
+ if !noconnect[iface.Name()] {
+ continue
+ }
+ expected := onClassic
+ comm := Commentf(iface.Name())
+
+ // check base declaration
+ cand := s.connectCand(c, iface.Name(), "", "")
+ err := cand.Check()
+
+ if expected {
+ c.Check(err, IsNil, comm)
+ } else {
+ c.Check(err, NotNil, comm)
+ }
+ }
+ }
+}
+
+func (s *baseDeclSuite) TestSanity(c *C) {
+ all := builtin.Interfaces()
+
+ // these interfaces have rules both for the slots and plugs side
+ // given how the rules work this can be delicate,
+ // listed here to make sure that was a conscious decision
+ bothSides := map[string]bool{
+ "docker-support": true,
+ "kernel-module-control": true,
+ "lxd-support": true,
+ "snapd-control": true,
+ }
+
+ for _, iface := range all {
+ plugRule := s.baseDecl.PlugRule(iface.Name())
+ slotRule := s.baseDecl.SlotRule(iface.Name())
+ if plugRule == nil && slotRule == nil {
+ c.Logf("%s is not considered in the base declaration", iface.Name())
+ c.Fail()
+ }
+ if plugRule != nil && slotRule != nil {
+ if !bothSides[iface.Name()] {
+ c.Logf("%s have both a base declaration slot rule and plug rule, make sure that's intended and correct", iface.Name())
+ c.Fail()
+ }
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const bluetoothControlConnectedPlugAppArmor = `
+# Description: Allow managing the kernel side Bluetooth stack. Reserved
+# because this gives privileged access to the system.
+# Usage: reserved
+
+ network bluetooth,
+ # For crypto functionality the kernel offers
+ network alg,
+
+ capability net_admin,
+
+ # File accesses
+ /sys/bus/usb/drivers/btusb/ r,
+ /sys/bus/usb/drivers/btusb/** r,
+ /sys/class/bluetooth/ r,
+ /sys/devices/**/bluetooth/ rw,
+ /sys/devices/**/bluetooth/** rw,
+
+ # Requires CONFIG_BT_VHCI to be loaded
+ /dev/vhci rw,
+`
+
+const bluetoothControlConnectedPlugSecComp = `
+# Description: Allow managing the kernel side Bluetooth stack. Reserved
+# because this gives privileged access to the system.
+# Usage: reserved
+bind
+getsockopt
+`
+
+func NewBluetoothControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "bluetooth-control",
+ connectedPlugAppArmor: bluetoothControlConnectedPlugAppArmor,
+ connectedPlugSecComp: bluetoothControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type BluetoothControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&BluetoothControlInterfaceSuite{
+ iface: builtin.NewBluetoothControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "bluetooth-control",
+ Interface: "bluetooth-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "bluetooth-control",
+ Interface: "bluetooth-control",
+ },
+ },
+})
+
+func (s *BluetoothControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "bluetooth-control")
+}
+
+func (s *BluetoothControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "bluetooth-control",
+ Interface: "bluetooth-control",
+ }})
+ c.Assert(err, ErrorMatches, "bluetooth-control slots are reserved for the operating system snap")
+}
+
+func (s *BluetoothControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *BluetoothControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "bluetooth-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "bluetooth-control"`)
+}
+
+func (s *BluetoothControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var bluezPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the bluez service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+ network bluetooth,
+
+ capability net_admin,
+ capability net_bind_service,
+
+ # File accesses
+ /sys/bus/usb/drivers/btusb/ r,
+ /sys/bus/usb/drivers/btusb/** r,
+ /sys/class/bluetooth/ r,
+ /sys/devices/**/bluetooth/ rw,
+ /sys/devices/**/bluetooth/** rw,
+ /sys/devices/**/id/chassis_type r,
+
+ # TODO: use snappy hardware assignment for this once LP: #1498917 is fixed
+ /dev/rfkill rw,
+
+ # DBus accesses
+ #include <abstractions/dbus-strict>
+ dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Request,Release}Name
+ peer=(name=org.freedesktop.DBus),
+
+ dbus (send)
+ bus=system
+ path=/org/freedesktop/*
+ interface=org.freedesktop.DBus.Properties
+ peer=(label=unconfined),
+
+ # Allow binding the service to the requested connection name
+ dbus (bind)
+ bus=system
+ name="org.bluez",
+
+ # Allow binding the service to the requested connection name
+ dbus (bind)
+ bus=system
+ name="org.bluez.obex",
+
+ # Allow traffic to/from our path and interface with any method
+ dbus (receive, send)
+ bus=system
+ path=/org/bluez{,/**}
+ interface=org.bluez.*,
+
+ # Allow traffic to/from org.freedesktop.DBus for bluez service
+ dbus (receive, send)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.**,
+ dbus (receive, send)
+ bus=system
+ path=/org/bluez{,/**}
+ interface=org.freedesktop.DBus.**,
+
+ # Allow access to hostname system service
+ dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.DBus.Properties
+ peer=(label=unconfined),
+`)
+
+var bluezConnectedPlugAppArmor = []byte(`
+# Description: Allow using bluez service. Reserved because this gives
+# privileged access to the bluez service.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Allow all access to bluez service
+dbus (receive, send)
+ bus=system
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ peer=(name=org.bluez, label=unconfined),
+
+dbus (send)
+ bus=system
+ peer=(name=org.bluez.obex, label=unconfined),
+
+dbus (receive)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=unconfined),
+
+dbus (receive)
+ bus=system
+ path=/org/bluez{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=unconfined),
+`)
+
+var bluezPermanentSlotSecComp = []byte(`
+# Description: Allow operating as the bluez service. Reserved because this
+# gives
+# privileged access to the system.
+# Usage: reserved
+accept
+accept4
+bind
+connect
+getpeername
+getsockname
+getsockopt
+listen
+recv
+recvfrom
+recvmmsg
+recvmsg
+send
+sendmmsg
+sendmsg
+sendto
+setsockopt
+shutdown
+socketpair
+socket
+`)
+
+var bluezConnectedPlugSecComp = []byte(`
+# Description: Allow using bluez service. Reserved because this gives
+# privileged access to the bluez service.
+# Usage: reserved
+
+# Can communicate with DBus system service
+connect
+getsockname
+recv
+recvmsg
+send
+sendto
+sendmsg
+socket
+`)
+
+var bluezPermanentSlotDBus = []byte(`
+<policy user="root">
+ <allow own="org.bluez"/>
+ <allow own="org.bluez.obex"/>
+ <allow send_destination="org.bluez"/>
+ <allow send_destination="org.bluez.obex"/>
+ <allow send_interface="org.bluez.Agent1"/>
+ <allow send_interface="org.bluez.ThermometerWatcher1"/>
+ <allow send_interface="org.bluez.AlertAgent1"/>
+ <allow send_interface="org.bluez.Profile1"/>
+ <allow send_interface="org.bluez.HeartRateWatcher1"/>
+ <allow send_interface="org.bluez.CyclingSpeedWatcher1"/>
+ <allow send_interface="org.bluez.GattCharacteristic1"/>
+ <allow send_interface="org.bluez.GattDescriptor1"/>
+ <allow send_interface="org.freedesktop.DBus.ObjectManager"/>
+ <allow send_interface="org.freedesktop.DBus.Properties"/>
+</policy>
+<policy context="default">
+ <deny send_destination="org.bluez"/>
+</policy>
+`)
+
+type BluezInterface struct{}
+
+func (iface *BluezInterface) Name() string {
+ return "bluez"
+}
+
+func (iface *BluezInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *BluezInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(bluezConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return bluezConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *BluezInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return bluezPermanentSlotAppArmor, nil
+ case interfaces.SecuritySecComp:
+ return bluezPermanentSlotSecComp, nil
+ case interfaces.SecurityDBus:
+ return bluezPermanentSlotDBus, nil
+ }
+ return nil, nil
+}
+
+func (iface *BluezInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *BluezInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *BluezInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *BluezInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type BluezInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&BluezInterfaceSuite{
+ iface: &builtin.BluezInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "bluez"},
+ Name: "bluez",
+ Interface: "bluez",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "bluez"},
+ Name: "bluezctl",
+ Interface: "bluez",
+ },
+ },
+})
+
+func (s *BluezInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "bluez")
+}
+
+// The label glob when all apps are bound to the bluez slot
+func (s *BluezInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "bluez",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "bluez",
+ Interface: "bluez",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.bluez.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the bluez slot
+func (s *BluezInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "bluez",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "bluez",
+ Interface: "bluez",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.bluez.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the bluez slot
+func (s *BluezInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "bluez",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "bluez",
+ Interface: "bluez",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.bluez.app"),`)
+}
+
+func (s *BluezInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// BoolFileInterface is the type of all the bool-file interfaces.
+type BoolFileInterface struct{}
+
+// String returns the same value as Name().
+func (iface *BoolFileInterface) String() string {
+ return iface.Name()
+}
+
+// Name returns the name of the bool-file interface.
+func (iface *BoolFileInterface) Name() string {
+ return "bool-file"
+}
+
+var boolFileGPIOValuePattern = regexp.MustCompile(
+ "^/sys/class/gpio/gpio[0-9]+/value$")
+var boolFileAllowedPathPatterns = []*regexp.Regexp{
+ // The brightness of standard LED class device
+ regexp.MustCompile("^/sys/class/leds/[^/]+/brightness$"),
+ // The value of standard exported GPIO
+ boolFileGPIOValuePattern,
+}
+
+// SanitizeSlot checks and possibly modifies a slot.
+// Valid "bool-file" slots must contain the attribute "path".
+func (iface *BoolFileInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return fmt.Errorf("bool-file must contain the path attribute")
+ }
+ path = filepath.Clean(path)
+ for _, pattern := range boolFileAllowedPathPatterns {
+ if pattern.MatchString(path) {
+ return nil
+ }
+ }
+ return fmt.Errorf("bool-file can only point at LED brightness or GPIO value")
+}
+
+// SanitizePlug checks and possibly modifies a plug.
+func (iface *BoolFileInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // NOTE: currently we don't check anything on the plug side.
+ return nil
+}
+
+// ConnectedSlotSnippet returns security snippet specific to a given connection between the bool-file slot and some plug.
+// Applications associated with the slot don't gain any extra permissions.
+func (iface *BoolFileInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// PermanentSlotSnippet returns security snippet permanently granted to bool-file slots.
+// Applications associated with the slot, if the slot is a GPIO, gain permission to export, unexport and set direction of any GPIO pin.
+func (iface *BoolFileInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ gpioSnippet := []byte(`
+/sys/class/gpio/export rw,
+/sys/class/gpio/unexport rw,
+/sys/class/gpio/gpio[0-9]+/direction rw,
+`)
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ // To provide GPIOs we need extra permissions to export/unexport and to
+ // set the direction of each pin.
+ if iface.isGPIO(slot) {
+ return gpioSnippet, nil
+ }
+ return nil, nil
+ }
+ return nil, nil
+}
+
+// ConnectedPlugSnippet returns security snippet specific to a given connection between the bool-file plug and some slot.
+// Applications associated with the plug gain permission to read, write and lock the designated file.
+func (iface *BoolFileInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ // Allow write and lock on the file designated by the path.
+ // Dereference symbolic links to file path handed out to apparmor since
+ // sysfs is full of symlinks and apparmor requires uses real path for
+ // filtering.
+ path, err := iface.dereferencedPath(slot)
+ if err != nil {
+ return nil, fmt.Errorf("cannot compute plug security snippet: %v", err)
+ }
+ return []byte(fmt.Sprintf("%s rwk,\n", path)), nil
+ }
+ return nil, nil
+}
+
+// PermanentPlugSnippet returns the configuration snippet required to use a bool-file interface.
+// Applications associated with the plug don't gain any extra permissions.
+func (iface *BoolFileInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *BoolFileInterface) dereferencedPath(slot *interfaces.Slot) (string, error) {
+ if path, ok := slot.Attrs["path"].(string); ok {
+ path, err := evalSymlinks(path)
+ if err != nil {
+ return "", err
+ }
+ return filepath.Clean(path), nil
+ }
+ panic("slot is not sanitized")
+}
+
+// isGPIO checks if a given bool-file slot refers to a GPIO pin.
+func (iface *BoolFileInterface) isGPIO(slot *interfaces.Slot) bool {
+ if path, ok := slot.Attrs["path"].(string); ok {
+ path = filepath.Clean(path)
+ return boolFileGPIOValuePattern.MatchString(path)
+ }
+ panic("slot is not sanitized")
+}
+
+// AutoConnect returns whether plug and slot should be implicitly
+// auto-connected assuming they will be an unambiguous connection
+// candidate and declaration-based checks allow.
+//
+// By default we allow what declarations allowed.
+func (iface *BoolFileInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type BoolFileInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+ gpioSlot *interfaces.Slot
+ ledSlot *interfaces.Slot
+ badPathSlot *interfaces.Slot
+ parentDirPathSlot *interfaces.Slot
+ missingPathSlot *interfaces.Slot
+ badInterfaceSlot *interfaces.Slot
+ plug *interfaces.Plug
+ badInterfacePlug *interfaces.Plug
+}
+
+var _ = Suite(&BoolFileInterfaceSuite{
+ iface: &builtin.BoolFileInterface{},
+})
+
+func (s *BoolFileInterfaceSuite) SetUpTest(c *C) {
+ info := snaptest.MockInfo(c, `
+name: ubuntu-core
+slots:
+ gpio:
+ interface: bool-file
+ path: /sys/class/gpio/gpio13/value
+ led:
+ interface: bool-file
+ path: "/sys/class/leds/input27::capslock/brightness"
+ missing-path: bool-file
+ bad-path:
+ interface: bool-file
+ path: path
+ parent-dir-path:
+ interface: bool-file
+ path: "/sys/class/gpio/../value"
+ bad-interface-slot: other-interface
+plugs:
+ plug: bool-file
+ bad-interface-plug: other-interface
+`, &snap.SideInfo{})
+ s.gpioSlot = &interfaces.Slot{SlotInfo: info.Slots["gpio"]}
+ s.ledSlot = &interfaces.Slot{SlotInfo: info.Slots["led"]}
+ s.missingPathSlot = &interfaces.Slot{SlotInfo: info.Slots["missing-path"]}
+ s.badPathSlot = &interfaces.Slot{SlotInfo: info.Slots["bad-path"]}
+ s.parentDirPathSlot = &interfaces.Slot{SlotInfo: info.Slots["parent-dir-path"]}
+ s.badInterfaceSlot = &interfaces.Slot{SlotInfo: info.Slots["bad-interface-slot"]}
+ s.plug = &interfaces.Plug{PlugInfo: info.Plugs["plug"]}
+ s.badInterfacePlug = &interfaces.Plug{PlugInfo: info.Plugs["bad-interface-plug"]}
+}
+
+func (s *BoolFileInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "bool-file")
+}
+
+func (s *BoolFileInterfaceSuite) TestSanitizeSlot(c *C) {
+ // Both LED and GPIO slots are accepted
+ err := s.iface.SanitizeSlot(s.ledSlot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(s.gpioSlot)
+ c.Assert(err, IsNil)
+ // Slots without the "path" attribute are rejected.
+ err = s.iface.SanitizeSlot(s.missingPathSlot)
+ c.Assert(err, ErrorMatches,
+ "bool-file must contain the path attribute")
+ // Slots without the "path" attribute are rejected.
+ err = s.iface.SanitizeSlot(s.parentDirPathSlot)
+ c.Assert(err, ErrorMatches,
+ "bool-file can only point at LED brightness or GPIO value")
+ // Slots with incorrect value of the "path" attribute are rejected.
+ err = s.iface.SanitizeSlot(s.badPathSlot)
+ c.Assert(err, ErrorMatches,
+ "bool-file can only point at LED brightness or GPIO value")
+ // It is impossible to use "bool-file" interface to sanitize slots with other interfaces.
+ c.Assert(func() { s.iface.SanitizeSlot(s.badInterfaceSlot) }, PanicMatches,
+ `slot is not of interface "bool-file"`)
+}
+
+func (s *BoolFileInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+ // It is impossible to use "bool-file" interface to sanitize plugs of different interface.
+ c.Assert(func() { s.iface.SanitizePlug(s.badInterfacePlug) }, PanicMatches,
+ `plug is not of interface "bool-file"`)
+}
+
+func (s *BoolFileInterfaceSuite) TestPlugSnippetHandlesSymlinkErrors(c *C) {
+ // Symbolic link traversal is handled correctly
+ builtin.MockEvalSymlinks(&s.BaseTest, func(path string) (string, error) {
+ return "", fmt.Errorf("broken symbolic link")
+ })
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.gpioSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, ErrorMatches, "cannot compute plug security snippet: broken symbolic link")
+ c.Assert(snippet, IsNil)
+}
+
+func (s *BoolFileInterfaceSuite) TestPlugSnippetDereferencesSymlinks(c *C) {
+ // Use a fake (successful) dereferencing function for the remainder of the test.
+ builtin.MockEvalSymlinks(&s.BaseTest, func(path string) (string, error) {
+ return "(dereferenced)" + path, nil
+ })
+ // Extra apparmor permission to access GPIO value
+ // The path uses dereferenced symbolic links.
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.gpioSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, []byte(
+ "(dereferenced)/sys/class/gpio/gpio13/value rwk,\n"))
+ // Extra apparmor permission to access LED brightness.
+ // The path uses dereferenced symbolic links.
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.ledSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, []byte(
+ "(dereferenced)/sys/class/leds/input27::capslock/brightness rwk,\n"))
+}
+
+func (s *BoolFileInterfaceSuite) TestPermanentPlugSecurityDoesNotContainSlotSecurity(c *C) {
+ // Use a fake (successful) dereferencing function for the remainder of the test.
+ builtin.MockEvalSymlinks(&s.BaseTest, func(path string) (string, error) {
+ return path, nil
+ })
+ var err error
+ var slotSnippet, plugSnippet []byte
+ plugSnippet, err = s.iface.PermanentPlugSnippet(s.plug, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ slotSnippet, err = s.iface.PermanentSlotSnippet(s.gpioSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ // Ensure that we don't accidentally give plug-side permissions to slot-side.
+ c.Assert(bytes.Contains(plugSnippet, slotSnippet), Equals, false)
+}
+
+func (s *BoolFileInterfaceSuite) TestConnectedPlugSnippetPanicksOnUnsanitizedSlots(c *C) {
+ // Unsanitized slots should never be used and cause a panic.
+ c.Assert(func() {
+ s.iface.ConnectedPlugSnippet(s.plug, s.missingPathSlot, interfaces.SecurityAppArmor)
+ }, PanicMatches, "slot is not sanitized")
+}
+
+func (s *BoolFileInterfaceSuite) TestConnectedPlugSnippetUnusedSecuritySystems(c *C) {
+ for _, slot := range []*interfaces.Slot{s.ledSlot, s.gpioSlot} {
+ // No extra seccomp permissions for plug
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ // No extra dbus permissions for plug
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ // No extra udev permissions for plug
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ // No extra udev permissions for plug
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ }
+}
+
+func (s *BoolFileInterfaceSuite) TestPermanentPlugSnippetUnusedSecuritySystems(c *C) {
+ // No extra seccomp permissions for plug
+ snippet, err := s.iface.PermanentPlugSnippet(s.plug, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ // No extra dbus permissions for plug
+ snippet, err = s.iface.PermanentPlugSnippet(s.plug, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ // No extra udev permissions for plug
+ snippet, err = s.iface.PermanentPlugSnippet(s.plug, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ // No extra udev permissions for plug
+ snippet, err = s.iface.PermanentPlugSnippet(s.plug, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *BoolFileInterfaceSuite) TestPermanentSlotSnippetGivesExtraPermissionsToConfigureGPIOs(c *C) {
+ // Extra apparmor permission to provide GPIOs
+ expectedGPIOSnippet := []byte(`
+/sys/class/gpio/export rw,
+/sys/class/gpio/unexport rw,
+/sys/class/gpio/gpio[0-9]+/direction rw,
+`)
+ snippet, err := s.iface.PermanentSlotSnippet(s.gpioSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedGPIOSnippet)
+}
+
+func (s *BoolFileInterfaceSuite) TestPermanentSlotSnippetGivesNoExtraPermissionsToConfigureLEDs(c *C) {
+ // No extra apparmor permission to provide LEDs
+ snippet, err := s.iface.PermanentSlotSnippet(s.ledSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *BoolFileInterfaceSuite) TestPermanentSlotSnippetPanicksOnUnsanitizedSlots(c *C) {
+ // Unsanitized slots should never be used and cause a panic.
+ c.Assert(func() {
+ s.iface.PermanentSlotSnippet(s.missingPathSlot, interfaces.SecurityAppArmor)
+ }, PanicMatches, "slot is not sanitized")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/log-observe
+const browserSupportConnectedPlugAppArmor = `
+# Description: Can access various APIs needed by modern browers (eg, Google
+# Chrome/Chromium and Mozilla) and file paths they expect. This interface is
+# transitional and is only in place while upstream's work to change their paths
+# and snappy is updated to properly mediate the APIs.
+# Usage: reserved
+
+# This allows raising the OOM score of other processes owned by the user.
+owner @{PROC}/@{pid}/oom_score_adj rw,
+
+# Chrome/Chromium should be fixed to honor TMPDIR or the snap packaging
+# adjusted to use LD_PRELOAD technique from LP: #1577514
+/var/tmp/ r,
+owner /var/tmp/etilqs_* rw,
+
+# Chrome/Chromium should be modified to use snap.$SNAP_NAME.* or the snap
+# packaging adjusted to use LD_PRELOAD technique from LP: #1577514
+owner /{dev,run}/shm/{,.}org.chromium.Chromium.* rw,
+owner /{dev,run}/shm/{,.}com.google.Chrome.* rw,
+
+# Chrome/Chromium should be adjusted to not use gconf. It is only used with
+# legacy systems that don't have snapd
+deny dbus (send)
+ bus=session
+ interface="org.gnome.GConf.Server",
+`
+
+const browserSupportConnectedPlugAppArmorWithoutSandbox = `
+# ptrace can be used to break out of the seccomp sandbox, but ps requests
+# 'ptrace (trace)' even though it isn't tracing other processes. Unfortunately,
+# this is due to the kernel overloading trace such that the LSMs are unable to
+# distinguish between tracing other processes and other accesses. We deny the
+# trace here to silence the log.
+# Note: for now, explicitly deny to avoid confusion and accidentally giving
+# away this dangerous access frivolously. We may conditionally deny this in the
+# future. If the kernel has https://lkml.org/lkml/2016/5/26/354 we could also
+# allow this.
+deny ptrace (trace) peer=snap.@{SNAP_NAME}.**,
+`
+
+const browserSupportConnectedPlugAppArmorWithSandbox = `
+# Leaks installed applications
+# TODO: should this be somewhere else?
+/etc/mailcap r,
+/usr/share/applications/{,*} r,
+/var/lib/snapd/desktop/applications/{,*} r,
+owner @{PROC}/@{pid}/fd/[0-9]* w,
+
+# Various files in /run/udev/data needed by Chrome Settings. Leaks device
+# information.
+# input
+/run/udev/data/c1:[0-9]* r, # /dev/psaux
+/run/udev/data/c10:[0-9]* r, # /dev/adbmouse
+/run/udev/data/c13:[0-9]* r, # /dev/input/*
+/run/udev/data/c180:[0-9]* r, # /dev/vrbuttons
+/run/udev/data/c4:[0-9]* r, # /dev/tty*, /dev/ttyS*
+/run/udev/data/c5:[0-9]* r, # /dev/tty, /dev/console, etc
+/run/udev/data/c7:[0-9]* r, # /dev/vcs*
+/run/udev/data/+hid:* r,
+/run/udev/data/+input:input[0-9]* r,
+
+# screen
+/run/udev/data/c29:[0-9]* r, # /dev/fb*
+/run/udev/data/+backlight:* r,
+/run/udev/data/+leds:* r,
+
+# sound
+/run/udev/data/c116:[0-9]* r, # alsa
+/run/udev/data/+sound:card[0-9]* r,
+
+# miscellaneous
+/run/udev/data/c108:[0-9]* r, # /dev/ppp
+/run/udev/data/c189:[0-9]* r, # USB serial converters
+/run/udev/data/c89:[0-9]* r, # /dev/i2c-*
+/run/udev/data/c81:[0-9]* r, # video4linux (/dev/video*, etc)
+/run/udev/data/+acpi:* r,
+/run/udev/data/+hwmon:hwmon[0-9]* r,
+/run/udev/data/+i2c:* r,
+/run/udev/data/+platform:* r,
+/sys/devices/**/bConfigurationValue r,
+/sys/devices/**/descriptors r,
+/sys/devices/**/manufacturer r,
+/sys/devices/**/product r,
+/sys/devices/**/serial r,
+
+# networking
+/run/udev/data/n[0-9]* r,
+/run/udev/data/+bluetooth:hci[0-9]* r,
+/run/udev/data/+rfkill:rfkill[0-9]* r,
+
+# storage
+/run/udev/data/b1:[0-9]* r, # /dev/ram*
+/run/udev/data/b7:[0-9]* r, # /dev/loop*
+/run/udev/data/b8:[0-9]* r, # /dev/sd*
+/run/udev/data/c21:[0-9]* r, # /dev/sg*
+/run/udev/data/+usb:[0-9]* r,
+
+# experimental
+/run/udev/data/b253:[0-9]* r,
+/run/udev/data/b259:[0-9]* r,
+/run/udev/data/c242:[0-9]* r,
+/run/udev/data/c243:[0-9]* r,
+/run/udev/data/c245:[0-9]* r,
+/run/udev/data/c246:[0-9]* r,
+/run/udev/data/c247:[0-9]* r,
+/run/udev/data/c248:[0-9]* r,
+/run/udev/data/c249:[0-9]* r,
+/run/udev/data/c250:[0-9]* r,
+/run/udev/data/c251:[0-9]* r,
+/run/udev/data/c254:[0-9]* r,
+
+/sys/bus/**/devices/ r,
+
+# Google Cloud Print
+unix (bind)
+ type=stream
+ addr="@[0-9A-F]*._service_*",
+
+# Policy needed only when using the chrome/chromium setuid sandbox
+capability sys_ptrace,
+ptrace (trace) peer=snap.@{SNAP_NAME}.**,
+unix (receive, send) peer=(label=snap.@{SNAP_NAME}.**),
+
+# If this were going to be allowed to all snaps, then for all the following
+# rules we would want to wrap in a 'browser_sandbox' profile, but a limitation
+# in AppArmor profile transitions prevents this.
+#
+# @{INSTALL_DIR}/@{SNAP_NAME}/@{SNAP_REVISION}/opt/google/chrome{,-beta,-unstable}/chrome-sandbox cx -> browser_sandbox,
+# profile browser_sandbox {
+# ...
+# # This rule needs to work but generates a parser error
+# @{INSTALL_DIR}/@{SNAP_NAME}/@{SNAP_REVISION}/opt/google/chrome/chrome px -> snap.@{SNAP_NAME}.@{SNAP_APP},
+# ...
+# }
+
+# Required for dropping into PID namespace. Keep in mind that until the
+# process drops this capability it can escape confinement, but once it
+# drops CAP_SYS_ADMIN we are ok.
+capability sys_admin,
+
+# All of these are for sanely dropping from root and chrooting
+capability chown,
+capability fsetid,
+capability setgid,
+capability setuid,
+capability sys_chroot,
+
+# User namespace sandbox
+owner @{PROC}/@{pid}/setgroups rw,
+owner @{PROC}/@{pid}/uid_map rw,
+owner @{PROC}/@{pid}/gid_map rw,
+
+# Webkit uses a particular SHM names # LP: 1578217
+owner /{dev,run}/shm/WK2SharedMemory.* rw,
+`
+
+const browserSupportConnectedPlugSecComp = `
+# Description: Can access various APIs needed by modern browers (eg, Google
+# Chrome/Chromium and Mozilla) and file paths they expect. This interface is
+# transitional and is only in place while upstream's work to change their paths
+# and snappy is updated to properly mediate the APIs.
+# Usage: reserved
+
+# for anonymous sockets
+bind
+listen
+accept
+
+# TODO: fine-tune when seccomp arg filtering available in stable distro
+# releases
+setpriority
+`
+
+const browserSupportConnectedPlugSecCompWithSandbox = `
+# Policy needed only when using the chrome/chromium setuid sandbox
+chroot
+# TODO: fine-tune when seccomp arg filtering available in stable distro
+# releases
+setuid
+setgid
+
+# Policy needed for Mozilla userns sandbox
+unshare
+quotactl
+`
+
+type BrowserSupportInterface struct{}
+
+func (iface *BrowserSupportInterface) Name() string {
+ return "browser-support"
+}
+
+func (iface *BrowserSupportInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *BrowserSupportInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+
+ // It's fine if allow-sandbox isn't specified, but it it is,
+ // it needs to be bool
+ if v, ok := plug.Attrs["allow-sandbox"]; ok {
+ if _, ok = v.(bool); !ok {
+ return fmt.Errorf("browser-support plug requires bool with 'allow-sandbox'")
+ }
+ }
+
+ return nil
+}
+
+func (iface *BrowserSupportInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *BrowserSupportInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *BrowserSupportInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ allowSandbox, _ := plug.Attrs["allow-sandbox"].(bool)
+
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ snippet := []byte(browserSupportConnectedPlugAppArmor)
+ if allowSandbox {
+ snippet = append(snippet, browserSupportConnectedPlugAppArmorWithSandbox...)
+ } else {
+ snippet = append(snippet, browserSupportConnectedPlugAppArmorWithoutSandbox...)
+ }
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ snippet := []byte(browserSupportConnectedPlugSecComp)
+ if allowSandbox {
+ snippet = append(snippet, browserSupportConnectedPlugSecCompWithSandbox...)
+ }
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *BrowserSupportInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *BrowserSupportInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type BrowserSupportInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&BrowserSupportInterfaceSuite{
+ iface: &builtin.BrowserSupportInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "browser-support",
+ Interface: "browser-support",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "browser-support",
+ Interface: "browser-support",
+ },
+ },
+})
+
+func (s *BrowserSupportInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "browser-support")
+}
+
+func (s *BrowserSupportInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestSanitizePlugNoAttrib(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestSanitizePlugWithAttrib(c *C) {
+ const mockSnapYaml = `name: browser-support-plug-snap
+version: 1.0
+plugs:
+ browser-support-plug:
+ interface: browser-support
+ allow-sandbox: true
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["browser-support-plug"]}
+ err := s.iface.SanitizePlug(plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestSanitizePlugWithBadAttrib(c *C) {
+ const mockSnapYaml = `name: browser-support-plug-snap
+version: 1.0
+plugs:
+ browser-support-plug:
+ interface: browser-support
+ allow-sandbox: bad
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["browser-support-plug"]}
+ err := s.iface.SanitizePlug(plug)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "browser-support plug requires bool with 'allow-sandbox'")
+}
+
+func (s *BrowserSupportInterfaceSuite) TestConnectedPlugSnippetWithoutAttrib(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`)
+ c.Assert(string(snippet), Not(testutil.Contains), `capability sys_admin,`)
+ c.Assert(string(snippet), testutil.Contains, `deny ptrace (trace) peer=snap.@{SNAP_NAME}.**`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`)
+ c.Assert(string(snippet), Not(testutil.Contains), `chroot`)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestConnectedPlugSnippetWithAttribFalse(c *C) {
+ const mockSnapYaml = `name: browser-support-plug-snap
+version: 1.0
+plugs:
+ browser-support-plug:
+ interface: browser-support
+ allow-sandbox: false
+`
+
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["browser-support-plug"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`)
+ c.Assert(string(snippet), Not(testutil.Contains), `capability sys_admin,`)
+ c.Assert(string(snippet), testutil.Contains, `deny ptrace (trace) peer=snap.@{SNAP_NAME}.**`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`)
+ c.Assert(string(snippet), Not(testutil.Contains), `chroot`)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestConnectedPlugSnippetWithAttribTrue(c *C) {
+ const mockSnapYaml = `name: browser-support-plug-snap
+version: 1.0
+plugs:
+ browser-support-plug:
+ interface: browser-support
+ allow-sandbox: true
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["browser-support-plug"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`)
+ c.Assert(string(snippet), testutil.Contains, `ptrace (trace) peer=snap.@{SNAP_NAME}.**`)
+ c.Assert(string(snippet), Not(testutil.Contains), `deny ptrace (trace) peer=snap.@{SNAP_NAME}.**`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `# Description: Can access various APIs needed by modern browers`)
+ c.Assert(string(snippet), testutil.Contains, `chroot`)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "browser-support"`)
+}
+
+func (s *BrowserSupportInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor and
+ // seccomp
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const cameraConnectedPlugAppArmor = `
+# Until we have proper device assignment, allow access to all cameras
+/dev/video[0-9]* rw,
+
+# Allow detection of cameras. Leaks plugged in USB device info
+/sys/bus/usb/devices/ r,
+/sys/devices/pci**/usb*/**/idVendor r,
+/sys/devices/pci**/usb*/**/idProduct r,
+/run/udev/data/c81:[0-9]* r, # video4linux (/dev/video*, etc)
+`
+
+// NewCameraInterface returns a new "camera" interface.
+func NewCameraInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "camera",
+ connectedPlugAppArmor: cameraConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+type evalSymlinksFn func(string) (string, error)
+
+// evalSymlinks is either filepath.EvalSymlinks or a mocked function for
+// applicable for testing.
+var evalSymlinks = filepath.EvalSymlinks
+
+type commonInterface struct {
+ name string
+ connectedPlugAppArmor string
+ connectedPlugSecComp string
+ connectedPlugKMod string
+ reservedForOS bool
+ rejectAutoConnectPairs bool
+}
+
+// Name returns the interface name.
+func (iface *commonInterface) Name() string {
+ return iface.name
+}
+
+// SanitizeSlot checks and possibly modifies a slot.
+//
+// If the reservedForOS flag is set then only slots on core snap
+// are allowed.
+func (iface *commonInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface.Name()))
+ }
+ if iface.reservedForOS && slot.Snap.Type != snap.TypeOS {
+ return fmt.Errorf("%s slots are reserved for the operating system snap", iface.name)
+ }
+ return nil
+}
+
+// SanitizePlug checks and possibly modifies a plug.
+func (iface *commonInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+ // NOTE: currently we don't check anything on the plug side.
+ return nil
+}
+
+// PermanentPlugSnippet returns the snippet of text for the given security
+// system that is used during the whole lifetime of affected applications,
+// whether the plug is connected or not.
+//
+// Plugs don't get any permanent security snippets.
+func (iface *commonInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// ConnectedPlugSnippet returns the snippet of text for the given security
+// system that is used by affected application, while a specific connection
+// between a plug and a slot exists.
+//
+// Connected plugs get the static seccomp and apparmor blobs defined by the
+// instance variables. They are not really connection specific in this case.
+func (iface *commonInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(iface.connectedPlugAppArmor), nil
+ case interfaces.SecuritySecComp:
+ return []byte(iface.connectedPlugSecComp), nil
+ case interfaces.SecurityKMod:
+ return []byte(iface.connectedPlugKMod), nil
+ }
+ return nil, nil
+}
+
+// PermanentSlotSnippet returns the snippet of text for the given security
+// system that is used during the whole lifetime of affected applications,
+// whether the slot is connected or not.
+//
+// Slots don't get any permanent security snippets.
+func (iface *commonInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// ConnectedSlotSnippet returns the snippet of text for the given security
+// system that is used by affected application, while a specific connection
+// between a plug and a slot exists.
+//
+// Slots don't get any per-connection security snippets.
+func (iface *commonInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// AutoConnect returns whether plug and slot should be implicitly
+// auto-connected assuming they will be an unambiguous connection
+// candidate and declaration-based checks allow.
+//
+// By default we allow what declarations allowed.
+func (iface *commonInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ return !iface.rejectAutoConnectPairs
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/testutil"
+)
+
+// MockEvalSymlinks replaces the path/filepath.EvalSymlinks function used inside the caps package.
+func MockEvalSymlinks(test *testutil.BaseTest, fn func(string) (string, error)) {
+ orig := evalSymlinks
+ evalSymlinks = fn
+ test.AddCleanup(func() {
+ evalSymlinks = orig
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+// ContentInterface allows sharing content between snaps
+type ContentInterface struct{}
+
+func (iface *ContentInterface) Name() string {
+ return "content"
+}
+
+func cleanSubPath(path string) bool {
+ return filepath.Clean(path) == path && path != ".." && !strings.HasPrefix(path, "../")
+}
+
+func (iface *ContentInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // check that we have either a read or write path
+ rpath := iface.path(slot, "read")
+ wpath := iface.path(slot, "write")
+ if len(rpath) == 0 && len(wpath) == 0 {
+ return fmt.Errorf("read or write path must be set")
+ }
+
+ // go over both paths
+ paths := rpath
+ paths = append(paths, wpath...)
+ for _, p := range paths {
+ if !cleanSubPath(p) {
+ return fmt.Errorf("content interface path is not clean: %q", p)
+ }
+ }
+
+ return nil
+}
+
+func (iface *ContentInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ target, ok := plug.Attrs["target"].(string)
+ if !ok || len(target) == 0 {
+ return fmt.Errorf("content plug must contain target path")
+ }
+ if !cleanSubPath(target) {
+ return fmt.Errorf("content interface target path is not clean: %q", target)
+ }
+
+ return nil
+}
+
+func (iface *ContentInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *ContentInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// path is an internal helper that extract the "read" and "write" attribute
+// of the slot
+func (iface *ContentInterface) path(slot *interfaces.Slot, name string) []string {
+ if name != "read" && name != "write" {
+ panic("internal error, path can only be used with read/write")
+ }
+
+ paths, ok := slot.Attrs[name].([]interface{})
+ if !ok {
+ return nil
+ }
+
+ out := make([]string, len(paths))
+ for i, p := range paths {
+ out[i], ok = p.(string)
+ if !ok {
+ return nil
+ }
+ }
+ return out
+}
+
+// resolveSpecialVariable resolves one of the three $SNAP* variables at the
+// beginning of a given path. The variables are $SNAP, $SNAP_DATA and
+// $SNAP_COMMON. If there are no variables then $SNAP is implicitly assumed
+// (this is the behavior that was used before the variables were supporter).
+func resolveSpecialVariable(path string, snapInfo *snap.Info) string {
+ if strings.HasPrefix(path, "$SNAP/") || path == "$SNAP" {
+ return strings.Replace(path, "$SNAP", snapInfo.MountDir(), 1)
+ }
+ if strings.HasPrefix(path, "$SNAP_DATA/") || path == "$SNAP_DATA" {
+ return strings.Replace(path, "$SNAP_DATA", snapInfo.DataDir(), 1)
+ }
+ if strings.HasPrefix(path, "$SNAP_COMMON/") || path == "$SNAP_COMMON" {
+ return strings.Replace(path, "$SNAP_COMMON", snapInfo.CommonDataDir(), 1)
+ }
+ // NOTE: assume $SNAP by default if nothing else is provided, for compatibility
+ return filepath.Join(snapInfo.MountDir(), path)
+}
+
+func mountEntry(plug *interfaces.Plug, slot *interfaces.Slot, relSrc string, mntOpts string) string {
+ dst := resolveSpecialVariable(plug.Attrs["target"].(string), plug.Snap)
+ src := resolveSpecialVariable(relSrc, slot.Snap)
+ return fmt.Sprintf("%s %s none bind%s 0 0", src, dst, mntOpts)
+}
+
+func (iface *ContentInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ contentSnippet := bytes.NewBuffer(nil)
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+
+ writePaths := iface.path(slot, "write")
+ if len(writePaths) > 0 {
+ fmt.Fprintf(contentSnippet, `
+# In addition to the bind mount, add any AppArmor rules so that
+# snaps may directly access the slot implementation's files. Due
+# to a limitation in the kernel's LSM hooks for AF_UNIX, these
+# are needed for using named sockets within the exported
+# directory.
+`)
+ for _, w := range writePaths {
+ fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n",
+ resolveSpecialVariable(w, slot.Snap))
+ }
+ }
+
+ readPaths := iface.path(slot, "read")
+ if len(readPaths) > 0 {
+ fmt.Fprintf(contentSnippet, `
+# In addition to the bind mount, add any AppArmor rules so that
+# snaps may directly access the slot implementation's files
+# read-only.
+`)
+ for _, r := range readPaths {
+ fmt.Fprintf(contentSnippet, "%s/** mrkix,\n",
+ resolveSpecialVariable(r, slot.Snap))
+ }
+ }
+
+ return contentSnippet.Bytes(), nil
+ case interfaces.SecurityMount:
+ for _, r := range iface.path(slot, "read") {
+ fmt.Fprintln(contentSnippet, mountEntry(plug, slot, r, ",ro"))
+ }
+ for _, w := range iface.path(slot, "write") {
+ fmt.Fprintln(contentSnippet, mountEntry(plug, slot, w, ""))
+ }
+ return contentSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+func (iface *ContentInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *ContentInterface) AutoConnect(plug *interfaces.Plug, slot *interfaces.Slot) bool {
+ return plug.Attrs["content"] == slot.Attrs["content"]
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type ContentSuite struct {
+ iface interfaces.Interface
+}
+
+var _ = Suite(&ContentSuite{
+ iface: &builtin.ContentInterface{},
+})
+
+func (s *ContentSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "content")
+}
+
+func (s *ContentSuite) TestSanitizeSlotSimple(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+slots:
+ content-slot:
+ interface: content
+ read:
+ - shared/read
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["content-slot"]}
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *ContentSuite) TestSanitizeSlotNoPaths(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+slots:
+ content-slot:
+ interface: content
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["content-slot"]}
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, ErrorMatches, "read or write path must be set")
+}
+
+func (s *ContentSuite) TestSanitizeSlotEmptyPaths(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+slots:
+ content-slot:
+ interface: content
+ read: []
+ write: []
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["content-slot"]}
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, ErrorMatches, "read or write path must be set")
+}
+
+func (s *ContentSuite) TestSanitizeSlotHasRealtivePath(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+slots:
+ content-slot:
+ interface: content
+`
+ for _, rw := range []string{"read: [../foo]", "write: [../bar]"} {
+ info := snaptest.MockInfo(c, mockSnapYaml+" "+rw, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["content-slot"]}
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, ErrorMatches, "content interface path is not clean:.*")
+ }
+}
+
+func (s *ContentSuite) TestSanitizePlugSimple(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+plugs:
+ content-plug:
+ interface: content
+ target: import
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["content-plug"]}
+ err := s.iface.SanitizePlug(plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *ContentSuite) TestSanitizePlugSimpleNoTarget(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+plugs:
+ content-plug:
+ interface: content
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["content-plug"]}
+ err := s.iface.SanitizePlug(plug)
+ c.Assert(err, ErrorMatches, "content plug must contain target path")
+}
+
+func (s *ContentSuite) TestSanitizePlugSimpleTargetRelative(c *C) {
+ const mockSnapYaml = `name: content-slot-snap
+version: 1.0
+plugs:
+ content-plug:
+ interface: content
+ target: ../foo
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["content-plug"]}
+ err := s.iface.SanitizePlug(plug)
+ c.Assert(err, ErrorMatches, "content interface target path is not clean:.*")
+}
+
+func (s *ContentSuite) TestResolveSpecialVariable(c *C) {
+ info := snaptest.MockInfo(c, "name: name", &snap.SideInfo{Revision: snap.R(42)})
+ c.Check(builtin.ResolveSpecialVariable("foo", info), Equals, "/snap/name/42/foo")
+ c.Check(builtin.ResolveSpecialVariable("$SNAP/foo", info), Equals, "/snap/name/42/foo")
+ c.Check(builtin.ResolveSpecialVariable("$SNAP_DATA/foo", info), Equals, "/var/snap/name/42/foo")
+ c.Check(builtin.ResolveSpecialVariable("$SNAP_COMMON/foo", info), Equals, "/var/snap/name/common/foo")
+ c.Check(builtin.ResolveSpecialVariable("$SNAP", info), Equals, "/snap/name/42")
+ c.Check(builtin.ResolveSpecialVariable("$SNAP_DATA", info), Equals, "/var/snap/name/42")
+ c.Check(builtin.ResolveSpecialVariable("$SNAP_COMMON", info), Equals, "/var/snap/name/common")
+}
+
+// Check that legacy syntax works and allows sharing read-only snap content
+func (s *ContentSuite) TestConnectedPlugSnippetSharingLegacy(c *C) {
+ const consumerYaml = `name: consumer
+plugs:
+ content:
+ target: import
+`
+ consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)})
+ plug := &interfaces.Plug{PlugInfo: consumerInfo.Plugs["content"]}
+ const producerYaml = `name: producer
+slots:
+ content:
+ read:
+ - export
+`
+ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)})
+ slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]}
+
+ content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount)
+ c.Assert(err, IsNil)
+ expected := "/snap/producer/5/export /snap/consumer/7/import none bind,ro 0 0\n"
+ c.Assert(string(content), Equals, expected)
+}
+
+// Check that sharing of read-only snap content is possible
+func (s *ContentSuite) TestConnectedPlugSnippetSharingSnap(c *C) {
+ const consumerYaml = `name: consumer
+plugs:
+ content:
+ target: $SNAP/import
+`
+ consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)})
+ plug := &interfaces.Plug{PlugInfo: consumerInfo.Plugs["content"]}
+ const producerYaml = `name: producer
+slots:
+ content:
+ read:
+ - $SNAP/export
+`
+ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)})
+ slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]}
+
+ content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount)
+ c.Assert(err, IsNil)
+ expected := "/snap/producer/5/export /snap/consumer/7/import none bind,ro 0 0\n"
+ c.Assert(string(content), Equals, expected)
+
+ content, err = s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ expected = `
+# In addition to the bind mount, add any AppArmor rules so that
+# snaps may directly access the slot implementation's files
+# read-only.
+/snap/producer/5/export/** mrkix,
+`
+ c.Assert(string(content), Equals, expected)
+}
+
+// Check that sharing of writable data is possible
+func (s *ContentSuite) TestConnectedPlugSnippetSharingSnapData(c *C) {
+ const consumerYaml = `name: consumer
+plugs:
+ content:
+ target: $SNAP_DATA/import
+`
+ consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)})
+ plug := &interfaces.Plug{PlugInfo: consumerInfo.Plugs["content"]}
+ const producerYaml = `name: producer
+slots:
+ content:
+ write:
+ - $SNAP_DATA/export
+`
+ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)})
+ slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]}
+
+ content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount)
+ c.Assert(err, IsNil)
+ expected := "/var/snap/producer/5/export /var/snap/consumer/7/import none bind 0 0\n"
+ c.Assert(string(content), Equals, expected)
+
+ content, err = s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ expected = `
+# In addition to the bind mount, add any AppArmor rules so that
+# snaps may directly access the slot implementation's files. Due
+# to a limitation in the kernel's LSM hooks for AF_UNIX, these
+# are needed for using named sockets within the exported
+# directory.
+/var/snap/producer/5/export/** mrwklix,
+`
+ c.Assert(string(content), Equals, expected)
+}
+
+// Check that sharing of writable common data is possible
+func (s *ContentSuite) TestConnectedPlugSnippetSharingSnapCommon(c *C) {
+ const consumerYaml = `name: consumer
+plugs:
+ content:
+ target: $SNAP_COMMON/import
+`
+ consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)})
+ plug := &interfaces.Plug{PlugInfo: consumerInfo.Plugs["content"]}
+ const producerYaml = `name: producer
+slots:
+ content:
+ write:
+ - $SNAP_COMMON/export
+`
+ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)})
+ slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]}
+
+ content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount)
+ c.Assert(err, IsNil)
+ expected := "/var/snap/producer/common/export /var/snap/consumer/common/import none bind 0 0\n"
+ c.Assert(string(content), Equals, expected)
+
+ content, err = s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ expected = `
+# In addition to the bind mount, add any AppArmor rules so that
+# snaps may directly access the slot implementation's files. Due
+# to a limitation in the kernel's LSM hooks for AF_UNIX, these
+# are needed for using named sockets within the exported
+# directory.
+/var/snap/producer/common/export/** mrwklix,
+`
+ c.Assert(string(content), Equals, expected)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const cupsControlConnectedPlugAppArmor = `
+# Description: Can access cups control socket. This is restricted because it provides
+# privileged access to configure printing.
+
+#include <abstractions/cups-client>
+`
+
+const cupsControlConnectedPlugSecComp = `
+recvfrom
+sendto
+setsockopt
+`
+
+// NewCupsControlInterface returns a new "cups" interface.
+func NewCupsControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "cups-control",
+ connectedPlugAppArmor: cupsControlConnectedPlugAppArmor,
+ connectedPlugSecComp: cupsControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+)
+
+const dbusPermanentSlotAppArmor = `
+# Description: Allow owning a name on DBus public bus
+
+#include <abstractions/###DBUS_ABSTRACTION###>
+
+# register on DBus
+dbus (send)
+ bus=###DBUS_BUS###
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="{Request,Release}Name"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=###DBUS_BUS###
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionUnix{ProcessID,User}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=###DBUS_BUS###
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionCredentials"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# bind to a well-known DBus name: ###DBUS_NAME###
+dbus (bind)
+ bus=###DBUS_BUS###
+ name=###DBUS_NAME###,
+
+# For KDE applications, also support alternation since they use org.kde.foo-PID
+# as their 'well-known' name. snapd does not allow declaring a 'well-known'
+# name that ends with '-[0-9]+', so this is ok.
+dbus (bind)
+ bus=###DBUS_BUS###
+ name=###DBUS_NAME###-[1-9]{,[0-9]}{,[0-9]}{,[0-9]}{,[0-9]}{,[0-9]},
+
+# Allow us to talk to dbus-daemon
+dbus (receive)
+ bus=###DBUS_BUS###
+ path=###DBUS_PATH###
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+dbus (send)
+ bus=###DBUS_BUS###
+ path=###DBUS_PATH###
+ interface=org.freedesktop.DBus.Properties
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+`
+
+const dbusPermanentSlotAppArmorClassic = `
+# allow unconfined clients to introspect us on classic
+dbus (receive)
+ bus=###DBUS_BUS###
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+
+# allow us to respond to unconfined clients via ###DBUS_INTERFACE###
+# on classic (send should be handled via another snappy interface).
+dbus (receive)
+ bus=###DBUS_BUS###
+ interface=###DBUS_INTERFACE###
+ peer=(label=unconfined),
+
+# allow us to respond to unconfined clients via ###DBUS_PATH### (eg,
+# org.freedesktop.*, org.gtk.Application, etc) on classic (send should be
+# handled via another snappy interface).
+dbus (receive)
+ bus=###DBUS_BUS###
+ path=###DBUS_PATH###
+ peer=(label=unconfined),
+`
+
+const dbusPermanentSlotSecComp = `
+# Description: Allow owning a name on DBus public bus
+getsockname
+recvmsg
+sendmsg
+sendto
+`
+
+const dbusPermanentSlotDBus = `
+<policy user="root">
+ <allow own="###DBUS_NAME###"/>
+ <allow send_destination="###DBUS_NAME###"/>
+</policy>
+<policy context="default">
+ <allow send_destination="###DBUS_NAME###"/>
+</policy>
+`
+
+const dbusConnectedSlotAppArmor = `
+# allow snaps to introspect us. This allows clients to introspect all
+# DBus interfaces of this service (but not use them).
+dbus (receive)
+ bus=###DBUS_BUS###
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# allow connected snaps to all paths via ###DBUS_INTERFACE###
+dbus (receive, send)
+ bus=###DBUS_BUS###
+ interface=###DBUS_INTERFACE###
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# allow connected snaps to all interfaces via ###DBUS_PATH### (eg,
+# org.freedesktop.*, org.gtk.Application, etc) to allow full integration with
+# connected snaps.
+dbus (receive, send)
+ bus=###DBUS_BUS###
+ path=###DBUS_PATH###
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`
+
+const dbusConnectedPlugAppArmor = `
+#include <abstractions/###DBUS_ABSTRACTION###>
+
+# allow snaps to introspect the slot servive. This allows us to introspect
+# all DBus interfaces of the service (but not use them).
+dbus (send)
+ bus=###DBUS_BUS###
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# allow connected snaps to ###DBUS_NAME###
+dbus (receive, send)
+ bus=###DBUS_BUS###
+ peer=(name=###DBUS_NAME###, label=###SLOT_SECURITY_TAGS###),
+# For KDE applications, also support alternation since they use org.kde.foo-PID
+# as their 'well-known' name. snapd does not allow ###DBUS_NAME### to end with
+# '-[0-9]+', so this is ok.
+dbus (receive, send)
+ bus=###DBUS_BUS###
+ peer=(name="###DBUS_NAME###-[1-9]{,[0-9]}{,[0-9]}{,[0-9]}{,[0-9]}{,[0-9]}", label=###SLOT_SECURITY_TAGS###),
+
+# allow connected snaps to all paths via ###DBUS_INTERFACE### to allow full
+# integration with connected snaps.
+dbus (receive, send)
+ bus=###DBUS_BUS###
+ interface=###DBUS_INTERFACE###
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# allow connected snaps to all interfaces via ###DBUS_PATH### (eg,
+# org.freedesktop.*, org.gtk.Application, etc) to allow full integration with
+# connected snaps.
+dbus (receive, send)
+ bus=###DBUS_BUS###
+ path=###DBUS_PATH###
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+const dbusConnectedPlugSecComp = `
+getsockname
+recvmsg
+sendmsg
+sendto
+`
+
+type DbusInterface struct{}
+
+func (iface *DbusInterface) Name() string {
+ return "dbus"
+}
+
+// Obtain yaml-specified bus well-known name
+func (iface *DbusInterface) getAttribs(attribs map[string]interface{}) (string, string, error) {
+ // bus attribute
+ bus, ok := attribs["bus"].(string)
+ if !ok {
+ return "", "", fmt.Errorf("cannot find attribute 'bus'")
+ }
+
+ if bus != "session" && bus != "system" {
+ return "", "", fmt.Errorf("bus '%s' must be one of 'session' or 'system'", bus)
+ }
+
+ // name attribute
+ name, ok := attribs["name"].(string)
+ if !ok {
+ return "", "", fmt.Errorf("cannot find attribute 'name'")
+ }
+
+ err := interfaces.ValidateDBusBusName(name)
+ if err != nil {
+ return "", "", err
+ }
+
+ // snapd has AppArmor rules (see above) allowing binds to busName-PID
+ // so to avoid overlap with different snaps (eg, busName running as PID
+ // 123 and busName-123), don't allow busName to end with -PID. If that
+ // rule is removed, this limitation can be lifted.
+ invalidSnappyBusName := regexp.MustCompile("-[0-9]+$")
+ if invalidSnappyBusName.MatchString(name) {
+ return "", "", fmt.Errorf("DBus bus name must not end with -NUMBER")
+ }
+
+ return bus, name, nil
+}
+
+// Determine AppArmor dbus abstraction to use based on bus
+func getAppArmorAbstraction(bus string) (string, error) {
+ var abstraction string
+ if bus == "system" {
+ abstraction = "dbus-strict"
+ } else if bus == "session" {
+ abstraction = "dbus-session-strict"
+ } else {
+ return "", fmt.Errorf("unknown abstraction for specified bus '%q'", bus)
+ }
+ return abstraction, nil
+}
+
+// Calculate individual snippet policy based on bus and name
+func getAppArmorSnippet(policy []byte, bus string, name string) []byte {
+ old := []byte("###DBUS_BUS###")
+ new := []byte(bus)
+ snippet := bytes.Replace(policy, old, new, -1)
+
+ old = []byte("###DBUS_NAME###")
+ new = []byte(name)
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ // convert name to AppArmor dbus path (eg 'org.foo' to '/org/foo{,/**}')
+ var pathBuf bytes.Buffer
+ pathBuf.WriteString(`"/`)
+ pathBuf.WriteString(strings.Replace(name, ".", "/", -1))
+ pathBuf.WriteString(`{,/**}"`)
+
+ old = []byte("###DBUS_PATH###")
+ new = pathBuf.Bytes()
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ // convert name to AppArmor dbus interface (eg, 'org.foo' to 'org.foo{,.*}')
+ var ifaceBuf bytes.Buffer
+ ifaceBuf.WriteString(`"`)
+ ifaceBuf.WriteString(name)
+ ifaceBuf.WriteString(`{,.*}"`)
+
+ old = []byte("###DBUS_INTERFACE###")
+ new = ifaceBuf.Bytes()
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ return snippet
+}
+
+func (iface *DbusInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *DbusInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ bus, name, err := iface.getAttribs(plug.Attrs)
+ if err != nil {
+ return nil, err
+ }
+
+ busSlot, nameSlot, err := iface.getAttribs(slot.Attrs)
+ if err != nil {
+ return nil, err
+ }
+
+ // ensure that we only connect to slot with matching attributes
+ if bus != busSlot || name != nameSlot {
+ return nil, nil
+ }
+
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ // well-known DBus name-specific connected plug policy
+ snippet := getAppArmorSnippet([]byte(dbusConnectedPlugAppArmor), bus, name)
+
+ // abstraction policy
+ abstraction, err := getAppArmorAbstraction(bus)
+ if err != nil {
+ return nil, err
+ }
+
+ old := []byte("###DBUS_ABSTRACTION###")
+ new := []byte(abstraction)
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ old = []byte("###SLOT_SECURITY_TAGS###")
+ new = slotAppLabelExpr(slot)
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return []byte(dbusConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *DbusInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ bus, name, err := iface.getAttribs(slot.Attrs)
+ if err != nil {
+ return nil, err
+ }
+
+ snippets := bytes.NewBufferString("")
+
+ // well-known DBus name-specific permanent slot policy
+ snippet := getAppArmorSnippet([]byte(dbusPermanentSlotAppArmor), bus, name)
+
+ // abstraction policy
+ abstraction, err := getAppArmorAbstraction(bus)
+ if err != nil {
+ return nil, err
+ }
+
+ old := []byte("###DBUS_ABSTRACTION###")
+ new := []byte(abstraction)
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ snippets.Write(snippet)
+
+ if release.OnClassic {
+ // classic-only policy
+ snippets.Write(getAppArmorSnippet([]byte(dbusPermanentSlotAppArmorClassic), bus, name))
+ }
+
+ return snippets.Bytes(), nil
+ case interfaces.SecuritySecComp:
+ return []byte(dbusPermanentSlotSecComp), nil
+ case interfaces.SecurityDBus:
+ bus, name, err := iface.getAttribs(slot.Attrs)
+ if err != nil {
+ return nil, err
+
+ }
+
+ // only system services need bus policy
+ if bus != "system" {
+ return nil, nil
+ }
+
+ old := []byte("###DBUS_NAME###")
+ new := []byte(name)
+ snippet := bytes.Replace([]byte(dbusPermanentSlotDBus), old, new, -1)
+
+ return []byte(snippet), nil
+ }
+ return nil, nil
+}
+
+func (iface *DbusInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ bus, name, err := iface.getAttribs(slot.Attrs)
+ if err != nil {
+ return nil, err
+ }
+
+ busPlug, namePlug, err := iface.getAttribs(plug.Attrs)
+ if err != nil {
+ return nil, err
+ }
+
+ // ensure that we only connect to slot with matching attributes. This
+ // makes sure that the security policy is correct, but does not ensure
+ // that 'snap interfaces' is correct.
+ // TODO: we can fix the 'snap interfaces' issue when interface/policy
+ // checkers when they are available
+ if bus != busPlug || name != namePlug {
+ return nil, nil
+ }
+
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ // well-known DBus name-specific connected slot policy
+ snippet := getAppArmorSnippet([]byte(dbusConnectedSlotAppArmor), bus, name)
+
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *DbusInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+
+ _, _, err := iface.getAttribs(plug.Attrs)
+ return err
+}
+
+func (iface *DbusInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ _, _, err := iface.getAttribs(slot.Attrs)
+ return err
+}
+
+func (iface *DbusInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type DbusInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+
+ sessionPlug *interfaces.Plug
+ systemPlug *interfaces.Plug
+ connectedSessionPlug *interfaces.Plug
+ connectedSystemPlug *interfaces.Plug
+
+ sessionSlot *interfaces.Slot
+ systemSlot *interfaces.Slot
+ connectedSessionSlot *interfaces.Slot
+ connectedSystemSlot *interfaces.Slot
+}
+
+var _ = Suite(&DbusInterfaceSuite{
+ iface: &builtin.DbusInterface{},
+})
+
+func (s *DbusInterfaceSuite) SetUpTest(c *C) {
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: test-dbus
+slots:
+ test-session-slot:
+ interface: dbus
+ bus: session
+ name: org.test-session-slot
+ test-system-slot:
+ interface: dbus
+ bus: system
+ name: org.test-system-slot
+ test-system-connected-slot:
+ interface: dbus
+ bus: system
+ name: org.test-system-connected
+ test-session-connected-slot:
+ interface: dbus
+ bus: session
+ name: org.test-session-connected
+
+plugs:
+ test-session-plug:
+ interface: dbus
+ bus: session
+ name: org.test-session-plug
+ test-system-plug:
+ interface: dbus
+ bus: system
+ name: org.test-system-plug
+ test-system-connected-plug:
+ interface: dbus
+ bus: system
+ name: org.test-system-connected
+ test-session-connected-plug:
+ interface: dbus
+ bus: session
+ name: org.test-session-connected
+
+apps:
+ test-session-provider:
+ slots:
+ - test-session-slot
+ test-system-provider:
+ slots:
+ - test-system-slot
+ test-session-consumer:
+ plugs:
+ - test-session-plug
+ test-system-consumer:
+ plugs:
+ - test-system-plug
+`))
+ c.Assert(err, IsNil)
+
+ s.sessionSlot = &interfaces.Slot{SlotInfo: info.Slots["test-session-slot"]}
+ s.systemSlot = &interfaces.Slot{SlotInfo: info.Slots["test-system-slot"]}
+ s.connectedSessionSlot = &interfaces.Slot{SlotInfo: info.Slots["test-session-connected-slot"]}
+ s.connectedSystemSlot = &interfaces.Slot{SlotInfo: info.Slots["test-system-connected-slot"]}
+
+ s.sessionPlug = &interfaces.Plug{PlugInfo: info.Plugs["test-session-plug"]}
+ s.systemPlug = &interfaces.Plug{PlugInfo: info.Plugs["test-system-plug"]}
+ s.connectedSessionPlug = &interfaces.Plug{PlugInfo: info.Plugs["test-session-connected-plug"]}
+ s.connectedSystemPlug = &interfaces.Plug{PlugInfo: info.Plugs["test-system-connected-plug"]}
+}
+
+func (s *DbusInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "dbus")
+}
+
+func (s *DbusInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ snippet, err = s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ snippet, err = s.iface.ConnectedSlotSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *DbusInterfaceSuite) TestValidSessionBusName(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: session
+ name: org.dbus-snap.session-a
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestValidSystemBusName(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: system
+ name: org.dbus-snap.system-a
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestValidFullBusName(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: system
+ name: org.dbus-snap.foo.bar.baz.n0rf_qux
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestNonexistentBusName(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: nonexistent
+ name: org.dbus-snap
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "bus 'nonexistent' must be one of 'session' or 'system'")
+}
+
+// If this test is failing, be sure to verify the AppArmor rules for binding to
+// a well-known name to avoid overlaps.
+func (s *DbusInterfaceSuite) TestInvalidBusNameEndsWithDashInt(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: session
+ name: org.dbus-snap.session-12345
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "DBus bus name must not end with -NUMBER")
+}
+
+func (s *DbusInterfaceSuite) TestSanitizeSlotSystem(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: system
+ name: org.dbus-snap.system
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestSanitizeSlotSession(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+slots:
+ dbus-slot:
+ interface: dbus
+ bus: session
+ name: org.dbus-snap.session
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ slot := &interfaces.Slot{SlotInfo: info.Slots["dbus-slot"]}
+ err = s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestSanitizePlugSystem(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+plugs:
+ dbus-plug:
+ interface: dbus
+ bus: system
+ name: org.dbus-snap.system
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["dbus-plug"]}
+ err = s.iface.SanitizePlug(plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestSanitizePlugSession(c *C) {
+ var mockSnapYaml = []byte(`name: dbus-snap
+version: 1.0
+plugs:
+ dbus-plug:
+ interface: dbus
+ bus: session
+ name: org.dbus-snap.session
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["dbus-plug"]}
+ err = s.iface.SanitizePlug(plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotAppArmorSession(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify abstraction rule
+ c.Check(string(snippet), testutil.Contains, "#include <abstractions/dbus-session-strict>\n")
+
+ // verify shared permanent slot policy
+ c.Check(string(snippet), testutil.Contains, "dbus (send)\n bus=session\n path=/org/freedesktop/DBus\n interface=org.freedesktop.DBus\n member=\"{Request,Release}Name\"\n peer=(name=org.freedesktop.DBus, label=unconfined),\n")
+
+ // verify individual bind rules
+ c.Check(string(snippet), testutil.Contains, "dbus (bind)\n bus=session\n name=org.test-session-slot,\n")
+
+ // verify individual path in rules
+ c.Check(string(snippet), testutil.Contains, "path=\"/org/test-session-slot{,/**}\"\n")
+
+ // verify interface in rule
+ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-session-slot{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotAppArmorSessionNative(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+ iface := &builtin.DbusInterface{}
+ snippet, err := iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify classic rule not present
+ c.Check(string(snippet), Not(testutil.Contains), "# allow us to respond to unconfined clients via \"org.test-session-slot{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotAppArmorSessionClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+ iface := &builtin.DbusInterface{}
+ snippet, err := iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify classic rule
+ c.Check(string(snippet), testutil.Contains, "# allow us to respond to unconfined clients via \"org.test-session-slot{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotAppArmorSystem(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.systemSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify abstraction rule
+ c.Check(string(snippet), testutil.Contains, "#include <abstractions/dbus-strict>\n")
+
+ // verify bind rule
+ c.Check(string(snippet), testutil.Contains, "dbus (bind)\n bus=system\n name=org.test-system-slot,\n")
+
+ // verify path in rule
+ c.Check(string(snippet), testutil.Contains, "path=\"/org/test-system-slot{,/**}\"\n")
+
+ // verify interface in rule
+ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-system-slot{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotSeccomp(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "getsockname\n")
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotDBusSession(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestPermanentSlotDBusSystem(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.systemSlot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "<policy user=\"root\">\n <allow own=\"org.test-system-slot\"/>")
+ c.Check(string(snippet), testutil.Contains, "<policy context=\"default\">\n <allow send_destination=\"org.test-system-slot\"/>")
+}
+
+func (s *DbusInterfaceSuite) TestConnectedSlotAppArmorSession(c *C) {
+ iface := &builtin.DbusInterface{}
+ snippet, err := iface.ConnectedSlotSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify introspectable rule
+ c.Check(string(snippet), testutil.Contains, "dbus (receive)\n bus=session\n interface=org.freedesktop.DBus.Introspectable\n member=Introspect\n peer=(label=\"snap.test-dbus.*\"),\n")
+
+ // verify bind rule not present
+ c.Check(string(snippet), Not(testutil.Contains), "dbus (bind)")
+
+ // verify individual path in rules
+ c.Check(string(snippet), testutil.Contains, "path=\"/org/test-session-connected{,/**}\"\n")
+
+ // verify interface in rule
+ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-session-connected{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestConnectedSlotAppArmorSystem(c *C) {
+ iface := &builtin.DbusInterface{}
+ snippet, err := iface.ConnectedSlotSnippet(s.connectedSystemPlug, s.connectedSystemSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify introspectable rule
+ c.Check(string(snippet), testutil.Contains, "dbus (receive)\n bus=system\n interface=org.freedesktop.DBus.Introspectable\n member=Introspect\n peer=(label=\"snap.test-dbus.*\"),\n")
+
+ // verify bind rule not present
+ c.Check(string(snippet), Not(testutil.Contains), "dbus (bind)")
+
+ // verify individual path in rules
+ c.Check(string(snippet), testutil.Contains, "path=\"/org/test-system-connected{,/**}\"\n")
+
+ // verify interface in rule
+ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-system-connected{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestConnectedPlugAppArmorSession(c *C) {
+ iface := &builtin.DbusInterface{}
+ snippet, err := iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify introspectable rule
+ c.Check(string(snippet), testutil.Contains, "dbus (send)\n bus=session\n interface=org.freedesktop.DBus.Introspectable\n member=Introspect\n peer=(label=\"snap.test-dbus.*\"),\n")
+
+ // verify bind rule not present
+ c.Check(string(snippet), Not(testutil.Contains), "dbus (bind)")
+
+ // verify well-known connection in rule
+ c.Check(string(snippet), testutil.Contains, "peer=(name=org.test-session-connected, label=")
+
+ // verify interface in rule
+
+ // verify individual path in rules
+ c.Check(string(snippet), testutil.Contains, "path=\"/org/test-session-connected{,/**}\"\n")
+
+ // verify interface in rule
+ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-session-connected{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestConnectedPlugAppArmorSystem(c *C) {
+ iface := &builtin.DbusInterface{}
+ snippet, err := iface.ConnectedPlugSnippet(s.connectedSystemPlug, s.connectedSystemSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify introspectable rule
+ c.Check(string(snippet), testutil.Contains, "dbus (send)\n bus=system\n interface=org.freedesktop.DBus.Introspectable\n member=Introspect\n peer=(label=\"snap.test-dbus.*\"),\n")
+
+ // verify bind rule not present
+ c.Check(string(snippet), Not(testutil.Contains), "dbus (bind)")
+
+ // verify well-known connection in rule
+ c.Check(string(snippet), testutil.Contains, "peer=(name=org.test-system-connected, label=")
+
+ // verify individual path in rules
+ c.Check(string(snippet), testutil.Contains, "path=\"/org/test-system-connected{,/**}\"\n")
+
+ // verify interface in rule
+ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-system-connected{,.*}\"\n")
+}
+
+func (s *DbusInterfaceSuite) TestConnectedPlugSeccomp(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "getsockname\n")
+}
+
+func (s *DbusInterfaceSuite) TestConnectionFirst(c *C) {
+ const plugYaml = `name: plugger
+version: 1.0
+plugs:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+`
+ const slotYaml = `name: slotter
+version: 1.0
+slots:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+ that:
+ interface: dbus
+ bus: system
+ name: org.slotter.other-session
+`
+
+ plugInfo := snaptest.MockInfo(c, plugYaml, nil)
+ matchingPlug := &interfaces.Plug{PlugInfo: plugInfo.Plugs["this"]}
+
+ slotInfo := snaptest.MockInfo(c, slotYaml, nil)
+ matchingSlot := &interfaces.Slot{SlotInfo: slotInfo.Slots["this"]}
+ nonmatchingSlot := &interfaces.Slot{SlotInfo: slotInfo.Slots["that"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(matchingPlug, matchingSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "org.slotter.session")
+ c.Check(string(snippet), testutil.Contains, "bus=session")
+ c.Check(string(snippet), Not(testutil.Contains), "org.slotter.other-session")
+ c.Check(string(snippet), Not(testutil.Contains), "bus=system")
+
+ snippet, err = s.iface.ConnectedPlugSnippet(matchingPlug, nonmatchingSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestConnectionSecond(c *C) {
+ const plugYaml = `name: plugger
+version: 1.0
+plugs:
+ that:
+ interface: dbus
+ bus: system
+ name: org.slotter.other-session
+`
+ const slotYaml = `name: slotter
+version: 1.0
+slots:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+ that:
+ interface: dbus
+ bus: system
+ name: org.slotter.other-session
+`
+
+ plugInfo := snaptest.MockInfo(c, plugYaml, nil)
+ matchingPlug := &interfaces.Plug{PlugInfo: plugInfo.Plugs["that"]}
+
+ slotInfo := snaptest.MockInfo(c, slotYaml, nil)
+ matchingSlot := &interfaces.Slot{SlotInfo: slotInfo.Slots["that"]}
+ nonmatchingSlot := &interfaces.Slot{SlotInfo: slotInfo.Slots["this"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(matchingPlug, matchingSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "org.slotter.other-session")
+ c.Check(string(snippet), testutil.Contains, "bus=system")
+ c.Check(string(snippet), Not(testutil.Contains), "org.slotter.session")
+ c.Check(string(snippet), Not(testutil.Contains), "bus=session")
+
+ snippet, err = s.iface.ConnectedPlugSnippet(matchingPlug, nonmatchingSlot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestConnectionBoth(c *C) {
+ const plugYaml = `name: plugger
+version: 1.0
+plugs:
+ that:
+ interface: dbus
+ bus: system
+ name: org.slotter.other-session
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+`
+ const slotYaml = `name: slotter
+version: 1.0
+slots:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+ that:
+ interface: dbus
+ bus: system
+ name: org.slotter.other-session
+`
+
+ plugInfo := snaptest.MockInfo(c, plugYaml, nil)
+ matchingPlug1 := &interfaces.Plug{PlugInfo: plugInfo.Plugs["this"]}
+ matchingPlug2 := &interfaces.Plug{PlugInfo: plugInfo.Plugs["that"]}
+
+ slotInfo := snaptest.MockInfo(c, slotYaml, nil)
+ matchingSlot1 := &interfaces.Slot{SlotInfo: slotInfo.Slots["this"]}
+ matchingSlot2 := &interfaces.Slot{SlotInfo: slotInfo.Slots["that"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(matchingPlug1, matchingSlot1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "org.slotter.session")
+ c.Check(string(snippet), testutil.Contains, "bus=session")
+
+ snippet, err = s.iface.ConnectedPlugSnippet(matchingPlug2, matchingSlot2, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "org.slotter.other-session")
+ c.Check(string(snippet), testutil.Contains, "bus=system")
+}
+
+func (s *DbusInterfaceSuite) TestConnectionMismatchBus(c *C) {
+ const plugYaml = `name: plugger
+version: 1.0
+plugs:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+`
+ const slotYaml = `name: slotter
+version: 1.0
+slots:
+ this:
+ interface: dbus
+ bus: system
+ name: org.slotter.session
+`
+
+ plugInfo := snaptest.MockInfo(c, plugYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: plugInfo.Plugs["this"]}
+
+ slotInfo := snaptest.MockInfo(c, slotYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: slotInfo.Slots["this"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *DbusInterfaceSuite) TestConnectionMismatchName(c *C) {
+ const plugYaml = `name: plugger
+version: 1.0
+plugs:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.session
+`
+ const slotYaml = `name: slotter
+version: 1.0
+slots:
+ this:
+ interface: dbus
+ bus: session
+ name: org.slotter.nomatch
+`
+
+ plugInfo := snaptest.MockInfo(c, plugYaml, nil)
+ plug := &interfaces.Plug{PlugInfo: plugInfo.Plugs["this"]}
+
+ slotInfo := snaptest.MockInfo(c, slotYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: slotInfo.Slots["this"]}
+
+ snippet, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// https://www.kernel.org/doc/Documentation/dcdbas.txt
+const dcdbasControlConnectedPlugAppArmor = `
+# Description: This interface allows communication with Dell Systems Management Base Driver
+# which provides a sysfs interface for systems management software such as Dell OpenManage
+# to perform system management interrupts and host control actions (system power cycle or
+# power off after OS shutdown) on certain Dell systems. The Dell libsmbios project aims
+# towards providing access to as much BIOS information as possible.
+#
+# See http://linux.dell.com/libsmbios/main/ for more information about the libsmbios project.
+
+# Usage: reserved
+
+# entries pertaining to System Management Interrupts (SMI)
+/sys/devices/platform/dcdbas/smi_data rw,
+/sys/devices/platform/dcdbas/smi_data_buf_phys_addr rw,
+/sys/devices/platform/dcdbas/smi_data_buf_size rw,
+/sys/devices/platform/dcdbas/smi_request rw,
+
+# entries pertaining to Host Control Action
+/sys/devices/platform/dcdbas/host_control_action rw,
+/sys/devices/platform/dcdbas/host_control_smi_type rw,
+/sys/devices/platform/dcdbas/host_control_on_shutdown rw,
+`
+
+// NewHardwareObserveInterface returns a new "dcdbas-control" interface.
+func NewDcdbasControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "dcdbas-control",
+ connectedPlugAppArmor: dcdbasControlConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type DcdbasControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&DcdbasControlInterfaceSuite{
+ iface: builtin.NewDcdbasControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "dcdbas-control",
+ Interface: "dcdbas-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "dcdbas-control",
+ Interface: "dcdbas-control",
+ },
+ },
+})
+
+func (s *DcdbasControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "dcdbas-control")
+}
+
+func (s *DcdbasControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "dcdbas-control",
+ Interface: "dcdbas-control",
+ }})
+ c.Assert(err, ErrorMatches, "dcdbas-control slots are reserved for the operating system snap")
+}
+
+func (s *DcdbasControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *DcdbasControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "dcdbas-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "dcdbas-control"`)
+}
+
+func (s *DcdbasControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Assert(string(snippet), testutil.Contains, `/dcdbas/smi_data`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const dockerConnectedPlugAppArmor = `
+# Description: allow access to the Docker daemon socket. This gives privileged
+# access to the system via Docker's socket API.
+# Usage: reserved
+
+# Allow talking to the docker daemon
+/{,var/}run/docker.sock rw,
+`
+
+const dockerConnectedPlugSecComp = `
+# Description: allow access to the Docker daemon socket. This gives privileged
+# access to the system via Docker's socket API.
+# Usage: reserved
+
+setsockopt
+bind
+`
+
+type DockerInterface struct{}
+
+func (iface *DockerInterface) Name() string {
+ return "docker"
+}
+
+func (iface *DockerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *DockerInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ snippet := []byte(dockerConnectedPlugAppArmor)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ snippet := []byte(dockerConnectedPlugSecComp)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *DockerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *DockerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ // The docker socket is a named socket and therefore mediated by
+ // AppArmor file rules. As such, there is no additional ConnectedSlot
+ // policy to add.
+ return nil, nil
+}
+
+func (iface *DockerInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *DockerInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *DockerInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const dockerSupportConnectedPlugAppArmor = `
+# Description: allow operating as the Docker daemon. This policy is
+# intentionally not restrictive and is here to help guard against programming
+# errors and not for security confinement. The Docker daemon by design requires
+# extensive access to the system and cannot be effectively confined against
+# malicious activity.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Allow sockets
+/{,var/}run/docker.sock rw,
+/{,var/}run/docker/ rw,
+/{,var/}run/docker/** mrwklix,
+/{,var/}run/runc/ rw,
+/{,var/}run/runc/** mrwklix,
+
+# Wide read access to /proc, but somewhat limited writes for now
+@{PROC}/ r,
+@{PROC}/** r,
+@{PROC}/[0-9]*/attr/exec w,
+@{PROC}/[0-9]*/oom_score_adj w,
+
+# Limited read access to specific bits of /sys
+/sys/kernel/mm/hugepages/ r,
+/sys/fs/cgroup/cpuset/cpuset.cpus r,
+/sys/fs/cgroup/cpuset/cpuset.mems r,
+/sys/module/apparmor/parameters/enabled r,
+
+# Limit cgroup writes a bit (Docker uses a "docker" sub-group)
+/sys/fs/cgroup/*/docker/ rw,
+/sys/fs/cgroup/*/docker/** rw,
+
+# Allow tracing ourself (especially the "runc" process we create)
+ptrace (trace) peer=@{profile_name},
+
+# Docker needs a lot of caps, but limits them in the app container
+capability,
+
+# Docker does all kinds of mounts all over the filesystem
+/dev/mapper/control rw,
+/dev/mapper/docker* rw,
+/dev/loop-control r,
+/dev/loop[0-9]* rw,
+/sys/devices/virtual/block/dm-[0-9]*/** r,
+mount,
+umount,
+
+# After doing a pivot_root using <graph-dir>/<container-fs>/.pivot_rootNNNNNN,
+# Docker removes the leftover /.pivot_rootNNNNNN directory (which is now
+# relative to "/" instead of "<graph-dir>/<container-fs>" thanks to pivot_root)
+pivot_root,
+/.pivot_root[0-9]*/ rw,
+
+# file descriptors (/proc/NNN/fd/X)
+# file descriptors in the container show up here due to attach_disconnected
+/[0-9]* rw,
+
+# Docker needs to be able to create and load the profile it applies to
+# containers ("docker-default")
+/sbin/apparmor_parser ixr,
+/etc/apparmor.d/cache/ r,
+/etc/apparmor.d/cache/.features r,
+/etc/apparmor.d/cache/docker* rw,
+/etc/apparmor/parser.conf r,
+/etc/apparmor/subdomain.conf r,
+/sys/kernel/security/apparmor/.replace rw,
+/sys/kernel/security/apparmor/{,**} r,
+
+# use 'privileged-containers: true' to support --security-opts
+change_profile -> docker-default,
+signal (send) peer=docker-default,
+ptrace (read, trace) peer=docker-default,
+
+# Graph (storage) driver bits
+/{dev,run}/shm/aufs.xino rw,
+/proc/fs/aufs/plink_maint w,
+/sys/fs/aufs/** r,
+
+#cf bug 1502785
+/ r,
+`
+
+const dockerSupportConnectedPlugSecComp = `
+# Description: allow operating as the Docker daemon. This policy is
+# intentionally not restrictive and is here to help guard against programming
+# errors and not for security confinement. The Docker daemon by design requires
+# extensive access to the system and cannot be effectively confined against
+# malicious activity.
+# Usage: reserved
+
+# Because seccomp may only go more strict, we must allow all syscalls to Docker
+# that it expects to give to containers in addition to what it needs to run and
+# trust that docker daemon # only gives out reasonable syscalls to containers.
+
+# Docker includes these in the default container whitelist, but they're
+# potentially dangerous.
+#finit_module
+#init_module
+#query_module
+#delete_module
+
+# These have a history of vulnerabilities, are not widely used, and
+# open_by_handle_at has been used to break out of Docker containers by brute
+# forcing the handle value: http://stealth.openwall.net/xSports/shocker.c
+#name_to_handle_at
+#open_by_handle_at
+
+# Calls the Docker daemon itself requires
+
+# /snap/docker/VERSION/bin/docker-runc
+# "do not inherit the parent's session keyring"
+# "make session keyring searcheable"
+# runC uses this to ensure the container doesn't have access to the host
+# keyring
+keyctl
+
+# /snap/docker/VERSION/bin/docker-runc
+pivot_root
+
+# ptrace can be abused to break out of the seccomp sandbox
+# but is required by the Docker daemon.
+ptrace
+
+# This list comes from Docker's default seccomp whitelist (which is applied to
+# all containers launched unless a custom profile is specified or
+# "--privileged" is used)
+# https://github.com/docker/docker/blob/v1.12.0/profiles/seccomp/seccomp_default.go#L39-L1879
+# It has been further filtered to exclude certain known-troublesome syscalls.
+accept
+accept4
+access
+acct
+adjtimex
+alarm
+arch_prctl
+bind
+bpf
+breakpoint
+brk
+cacheflush
+capget
+capset
+chdir
+chmod
+chown
+chown32
+chroot
+clock_getres
+clock_gettime
+clock_nanosleep
+clone
+close
+connect
+copy_file_range
+creat
+dup
+dup2
+dup3
+epoll_create
+epoll_create1
+epoll_ctl
+epoll_ctl_old
+epoll_pwait
+epoll_wait
+epoll_wait_old
+eventfd
+eventfd2
+execve
+execveat
+exit
+exit_group
+faccessat
+fadvise64
+fadvise64_64
+fallocate
+fanotify_init
+fanotify_mark
+fchdir
+fchmod
+fchmodat
+fchown
+fchown32
+fchownat
+fcntl
+fcntl64
+fdatasync
+fgetxattr
+flistxattr
+flock
+fork
+fremovexattr
+fsetxattr
+fstat
+fstat64
+fstatat64
+fstatfs
+fstatfs64
+fsync
+ftruncate
+ftruncate64
+futex
+futimesat
+getcpu
+getcwd
+getdents
+getdents64
+getegid
+getegid32
+geteuid
+geteuid32
+getgid
+getgid32
+getgroups
+getgroups32
+getitimer
+getpeername
+getpgid
+getpgrp
+getpid
+getppid
+getpriority
+getrandom
+getresgid
+getresgid32
+getresuid
+getresuid32
+getrlimit
+get_robust_list
+getrusage
+getsid
+getsockname
+getsockopt
+get_thread_area
+gettid
+gettimeofday
+getuid
+getuid32
+getxattr
+inotify_add_watch
+inotify_init
+inotify_init1
+inotify_rm_watch
+io_cancel
+ioctl
+io_destroy
+io_getevents
+ioperm
+iopl
+ioprio_get
+ioprio_set
+io_setup
+io_submit
+ipc
+kcmp
+kill
+lchown
+lchown32
+lgetxattr
+link
+linkat
+listen
+listxattr
+llistxattr
+_llseek
+lookup_dcookie
+lremovexattr
+lseek
+lsetxattr
+lstat
+lstat64
+madvise
+memfd_create
+mincore
+mkdir
+mkdirat
+mknod
+mknodat
+mlock
+mlock2
+mlockall
+mmap
+mmap2
+modify_ldt
+mount
+mprotect
+mq_getsetattr
+mq_notify
+mq_open
+mq_timedreceive
+mq_timedsend
+mq_unlink
+mremap
+msgctl
+msgget
+msgrcv
+msgsnd
+msync
+munlock
+munlockall
+munmap
+nanosleep
+newfstatat
+_newselect
+open
+openat
+pause
+perf_event_open
+personality
+pipe
+pipe2
+poll
+ppoll
+prctl
+pread64
+preadv
+prlimit64
+process_vm_readv
+process_vm_writev
+pselect6
+pwrite64
+pwritev
+read
+readahead
+readlink
+readlinkat
+readv
+reboot
+recv
+recvfrom
+recvmmsg
+recvmsg
+remap_file_pages
+removexattr
+rename
+renameat
+renameat2
+restart_syscall
+rmdir
+rt_sigaction
+rt_sigpending
+rt_sigprocmask
+rt_sigqueueinfo
+rt_sigreturn
+rt_sigsuspend
+rt_sigtimedwait
+rt_tgsigqueueinfo
+s390_pci_mmio_read
+s390_pci_mmio_write
+s390_runtime_instr
+sched_getaffinity
+sched_getattr
+sched_getparam
+sched_get_priority_max
+sched_get_priority_min
+sched_getscheduler
+sched_rr_get_interval
+sched_setaffinity
+sched_setattr
+sched_setparam
+sched_setscheduler
+sched_yield
+seccomp
+select
+semctl
+semget
+semop
+semtimedop
+send
+sendfile
+sendfile64
+sendmmsg
+sendmsg
+sendto
+setdomainname
+setfsgid
+setfsgid32
+setfsuid
+setfsuid32
+setgid
+setgid32
+setgroups
+setgroups32
+sethostname
+setitimer
+setns
+setpgid
+setpriority
+setregid
+setregid32
+setresgid
+setresgid32
+setresuid
+setresuid32
+setreuid
+setreuid32
+setrlimit
+set_robust_list
+setsid
+setsockopt
+set_thread_area
+set_tid_address
+settimeofday
+set_tls
+setuid
+setuid32
+setxattr
+shmat
+shmctl
+shmdt
+shmget
+shutdown
+sigaltstack
+signalfd
+signalfd4
+sigreturn
+socket
+socketcall
+socketpair
+splice
+stat
+stat64
+statfs
+statfs64
+stime
+symlink
+symlinkat
+sync
+sync_file_range
+syncfs
+sysinfo
+syslog
+tee
+tgkill
+time
+timer_create
+timer_delete
+timerfd_create
+timerfd_gettime
+timerfd_settime
+timer_getoverrun
+timer_gettime
+timer_settime
+times
+tkill
+truncate
+truncate64
+ugetrlimit
+umask
+umount
+umount2
+uname
+unlink
+unlinkat
+unshare
+utime
+utimensat
+utimes
+vfork
+vhangup
+vmsplice
+wait4
+waitid
+waitpid
+write
+writev
+`
+
+const dockerSupportPrivilegedAppArmor = `
+# Description: allow docker daemon to run privileged containers. This gives
+# full access to all resources on the system and thus gives device ownership to
+# connected snaps.
+
+# These rules are here to allow Docker to launch unconfined containers but
+# allow the docker daemon itself to go unconfined. Since it runs as root, this
+# grants device ownership.
+change_profile -> *,
+signal (send) peer=unconfined,
+ptrace (read, trace) peer=unconfined,
+
+# This grants raw access to device files and thus device ownership
+/dev/** mrwkl,
+@{PROC}/** mrwkl,
+`
+
+const dockerSupportPrivilegedSecComp = `
+# Description: allow docker daemon to run privileged containers. This gives
+# full access to all resources on the system and thus gives device ownership to
+# connected snaps.
+
+# This grants, among other things, kernel module loading and therefore device
+# ownership.
+@unrestricted
+`
+
+type DockerSupportInterface struct{}
+
+func (iface *DockerSupportInterface) Name() string {
+ return "docker-support"
+}
+
+func (iface *DockerSupportInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *DockerSupportInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ privileged, _ := plug.Attrs["privileged-containers"].(bool)
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ snippet := []byte(dockerSupportConnectedPlugAppArmor)
+ if privileged {
+ snippet = append(snippet, dockerSupportPrivilegedAppArmor...)
+ }
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ snippet := []byte(dockerSupportConnectedPlugSecComp)
+ if privileged {
+ snippet = append(snippet, dockerSupportPrivilegedSecComp...)
+ }
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *DockerSupportInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *DockerSupportInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *DockerSupportInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *DockerSupportInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+ if v, ok := plug.Attrs["privileged-containers"]; ok {
+ if _, ok = v.(bool); !ok {
+ return fmt.Errorf("docker-support plug requires bool with 'privileged-containers'")
+ }
+ }
+ return nil
+}
+
+func (iface *DockerSupportInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type DockerSupportInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&DockerSupportInterfaceSuite{
+ iface: &builtin.DockerSupportInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "core",
+ Type: snap.TypeOS},
+ Name: "docker-support",
+ Interface: "docker-support",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "docker",
+ },
+ Name: "docker-support",
+ Interface: "docker-support",
+ },
+ },
+})
+
+func (s *DockerSupportInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "docker-support")
+}
+
+func (s *DockerSupportInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *DockerSupportInterfaceSuite) TestConnectedPlugSnippet(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `pivot_root`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `pivot_root`)
+}
+
+func (s *DockerSupportInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DockerSupportInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedTrue(c *C) {
+ var mockSnapYaml = []byte(`name: docker
+version: 1.0
+plugs:
+ privileged:
+ interface: docker-support
+ privileged-containers: true
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["privileged"]}
+ err = s.iface.SanitizePlug(plug)
+ c.Assert(err, IsNil)
+
+ snippet, err := s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `change_profile -> *,`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `@unrestricted`)
+}
+
+func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedFalse(c *C) {
+ var mockSnapYaml = []byte(`name: docker
+version: 1.0
+plugs:
+ privileged:
+ interface: docker-support
+ privileged-containers: false
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["privileged"]}
+ err = s.iface.SanitizePlug(plug)
+ c.Assert(err, IsNil)
+
+ snippet, err := s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), Not(testutil.Contains), `change_profile -> *,`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), Not(testutil.Contains), `@unrestricted`)
+}
+
+func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedBad(c *C) {
+ var mockSnapYaml = []byte(`name: docker
+version: 1.0
+plugs:
+ privileged:
+ interface: docker-support
+ privileged-containers: bad
+`)
+
+ info, err := snap.InfoFromSnapYaml(mockSnapYaml)
+ c.Assert(err, IsNil)
+
+ plug := &interfaces.Plug{PlugInfo: info.Plugs["privileged"]}
+ err = s.iface.SanitizePlug(plug)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "docker-support plug requires bool with 'privileged-containers'")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type DockerInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&DockerInterfaceSuite{
+ iface: &builtin.DockerInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "docker",
+ },
+ Name: "docker-daemon",
+ Interface: "docker",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "docker"},
+ Name: "docker-client",
+ Interface: "docker",
+ },
+ },
+})
+
+func (s *DockerInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "docker")
+}
+
+func (s *DockerInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *DockerInterfaceSuite) TestConnectedPlugSnippet(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `run/docker.sock`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `bind`)
+}
+
+func (s *DockerInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *DockerInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+func MprisGetName(iface *MprisInterface, attribs map[string]interface{}) (string, error) {
+ return iface.getName(attribs)
+}
+
+var ResolveSpecialVariable = resolveSpecialVariable
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/firewall-control
+const firewallControlConnectedPlugAppArmor = `
+# Description: Can configure firewall. This is restricted because it gives
+# privileged access to networking and should only be used with trusted apps.
+# Usage: reserved
+
+#include <abstractions/nameservice>
+
+capability net_admin,
+
+/{,usr/}{,s}bin/iptables{,-save,-restore} ixr,
+/{,usr/}{,s}bin/ip6tables{,-save,-restore} ixr,
+/{,usr/}{,s}bin/iptables-apply ixr,
+/{,usr/}{,s}bin/xtables-multi ixr, # ip[6]tables*
+
+# ping - child profile would be nice but seccomp causes problems with that
+/{,usr/}{,s}bin/ping ixr,
+/{,usr/}{,s}bin/ping6 ixr,
+capability net_raw,
+capability setuid,
+network inet raw,
+network inet6 raw,
+
+# iptables (note, we don't want to allow loading modules, but
+# we can allow reading @{PROC}/sys/kernel/modprobe). Also,
+# snappy needs to have iptable_filter and ip6table_filter loaded,
+# they don't autoload.
+unix (bind) type=stream addr="@xtables",
+/{,var/}run/xtables.lock rwk,
+@{PROC}/sys/kernel/modprobe r,
+
+@{PROC}/@{pid}/net/ r,
+@{PROC}/@{pid}/net/** r,
+
+# sysctl
+/{,usr/}{,s}bin/sysctl ixr,
+@{PROC}/sys/ r,
+@{PROC}/sys/net/ r,
+@{PROC}/sys/net/core/ r,
+@{PROC}/sys/net/core/** r,
+@{PROC}/sys/net/ipv{4,6}/ r,
+@{PROC}/sys/net/ipv{4,6}/** r,
+@{PROC}/sys/net/netfilter/ r,
+@{PROC}/sys/net/netfilter/** r,
+@{PROC}/sys/net/nf_conntrack_max r,
+
+# various firewall related sysctl files
+@{PROC}/sys/net/ipv4/conf/*/rp_filter w,
+@{PROC}/sys/net/ipv{4,6}/conf/*/accept_source_route w,
+@{PROC}/sys/net/ipv{4,6}/conf/*/accept_redirects w,
+@{PROC}/sys/net/ipv4/icmp_echo_ignore_broadcasts w,
+@{PROC}/sys/net/ipv4/icmp_ignore_bogus_error_responses w,
+@{PROC}/sys/net/ipv4/icmp_echo_ignore_all w,
+@{PROC}/sys/net/ipv4/ip_forward w,
+@{PROC}/sys/net/ipv4/conf/*/log_martians w,
+@{PROC}/sys/net/ipv4/tcp_syncookies w,
+@{PROC}/sys/net/ipv6/conf/*/forwarding w,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/firewall-control
+const firewallControlConnectedPlugSecComp = `
+# Description: Can configure firewall. This is restricted because it gives
+# privileged access to networking and should only be used with trusted apps.
+# Usage: reserved
+
+# for connecting to xtables abstract socket
+bind
+connect
+getsockname
+getsockopt
+recv
+recvfrom
+recvmsg
+recvmmsg
+send
+sendmmsg
+sendmsg
+sendto
+setsockopt
+socket
+
+# for ping and ping6
+capset
+setuid
+`
+
+const firewallControlConnectedPlugKmod = `
+ip6table_filter
+iptable_filter
+`
+
+// NewFirewallControlInterface returns a new "firewall-control" interface.
+func NewFirewallControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "firewall-control",
+ connectedPlugAppArmor: firewallControlConnectedPlugAppArmor,
+ connectedPlugSecComp: firewallControlConnectedPlugSecComp,
+ connectedPlugKMod: firewallControlConnectedPlugKmod,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type FirewallControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&FirewallControlInterfaceSuite{
+ iface: builtin.NewFirewallControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "firewall-control",
+ Interface: "firewall-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "firewall-control",
+ Interface: "firewall-control",
+ },
+ },
+})
+
+func (s *FirewallControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "firewall-control")
+}
+
+func (s *FirewallControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "firewall-control",
+ Interface: "firewall-control",
+ }})
+ c.Assert(err, ErrorMatches, "firewall-control slots are reserved for the operating system snap")
+}
+
+func (s *FirewallControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *FirewallControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "firewall-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "firewall-control"`)
+}
+
+func (s *FirewallControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for kmod
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityKMod)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const fuseSupportConnectedPlugSecComp = `
+# Description: Can run a FUSE filesystem. Unprivileged fuse mounts are
+# not supported at this time.
+
+mount
+# for communicating with kernel
+recvmsg
+`
+
+const fuseSupportConnectedPlugAppArmor = `
+# Description: Can run a FUSE filesystem. Unprivileged fuse mounts are
+# not supported at this time.
+
+# Allow communicating with fuse kernel driver
+# https://www.kernel.org/doc/Documentation/filesystems/fuse.txt
+/dev/fuse rw,
+
+# Required for mounts
+capability sys_admin,
+
+# Allow mounts to our snap-specific writable directories
+# Note 1: fstype is 'fuse.<command>', eg 'fuse.sshfs'
+# Note 2: due to LP: #1612393 - @{HOME} can't be used in mountpoint
+# Note 3: local fuse mounts of filesystem directories are mediated by
+# AppArmor. The actual underlying file in the source directory is
+# mediated, not the presentation layer of the target directory, so
+# we can safely allow all local mounts to our snap-specific writable
+# directories.
+# Note 4: fuse supports a lot of different mount options, and applications
+# are not obligated to use fusermount to mount fuse filesystems, so
+# be very strict and only support the default (rw,nosuid,nodev) and
+# read-only.
+mount fstype=fuse.* options=(ro,nosuid,nodev) ** -> /home/*/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
+mount fstype=fuse.* options=(rw,nosuid,nodev) ** -> /home/*/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
+mount fstype=fuse.* options=(ro,nosuid,nodev) ** -> /var/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
+mount fstype=fuse.* options=(rw,nosuid,nodev) ** -> /var/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
+
+# Explicitly deny reads to /etc/fuse.conf. We do this to ensure that
+# the safe defaults of fuse are used (which are enforced by our mount
+# rules) and not system-specific options from /etc/fuse.conf that
+# may conflict with our mount rules.
+deny /etc/fuse.conf r,
+
+# Allow read access to the fuse filesystem
+/sys/fs/fuse/ r,
+/sys/fs/fuse/** r,
+
+# Unprivileged fuser mounts must use the setuid helper in the core snap
+# (not currently available, so don't include in policy at this time).
+#/{,usr/}bin/fusermount ixr,
+`
+
+// NewFuseControlInterface returns a new "fuse-support" interface.
+func NewFuseSupportInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "fuse-support",
+ connectedPlugAppArmor: fuseSupportConnectedPlugAppArmor,
+ connectedPlugSecComp: fuseSupportConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type FuseSupportInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&FuseSupportInterfaceSuite{
+ iface: builtin.NewFuseSupportInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "fuse-support",
+ Interface: "fuse-support",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "fuse-support",
+ Interface: "fuse-support",
+ },
+ },
+})
+
+func (s *FuseSupportInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "fuse-support")
+}
+
+func (s *FuseSupportInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "fuse-support",
+ Interface: "fuse-support",
+ }})
+ c.Assert(err, ErrorMatches, "fuse-support slots are reserved for the operating system snap")
+}
+
+func (s *FuseSupportInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *FuseSupportInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "fuse-support"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "fuse-support"`)
+}
+
+func (s *FuseSupportInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var fwupdPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the fwupd service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+ # Allow read/write access for old efivars sysfs interface
+ capability sys_admin,
+ # Allow libfwup to access efivarfs with immutable flag
+ capability linux_immutable,
+
+ # For udev
+ network netlink raw,
+
+ # File accesses
+ # Allow access for EFI System Resource Table in the UEFI 2.5+ specification
+ /sys/firmware/efi/esrt/entries/ r,
+ /sys/firmware/efi/esrt/entries/** r,
+
+ # Allow fwupd to access system information
+ /sys/devices/virtual/dmi/id/product_name r,
+ /sys/devices/virtual/dmi/id/sys_vendor r,
+
+ # Allow read/write access for efivarfs filesystem
+ /sys/firmware/efi/efivars/ r,
+ /sys/firmware/efi/efivars/** rw,
+
+ # Allow write access for efi firmware updater
+ /boot/efi/EFI/ubuntu/fw/** rw,
+
+ # Allow access from efivar library
+ owner @{PROC}/@{pid}/mounts r,
+ /sys/devices/{pci*,platform}/**/block/**/partition r,
+ # Introspect the block devices to get partition guid and size information
+ /run/udev/data/b[0-9]*:[0-9]* r,
+
+ # Allow access UEFI firmware platform size
+ /sys/firmware/efi/ r,
+ /sys/firmware/efi/fw_platform_size r,
+
+ # DBus accesses
+ #include <abstractions/dbus-strict>
+ dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Request,Release}Name
+ peer=(name=org.freedesktop.DBus),
+
+ dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member=GetConnectionUnixUser
+ peer=(label=unconfined),
+
+ # Allow binding the service to the requested connection name
+ dbus (bind)
+ bus=system
+ name="org.freedesktop.fwupd",
+`)
+
+var fwupdConnectedPlugAppArmor = []byte(`
+# Description: Allow using fwupd service. Reserved because this gives
+# privileged access to the fwupd service.
+# Usage: reserved
+
+ #Can access the network
+ #include <abstractions/nameservice>
+ #include <abstractions/ssl_certs>
+
+ # DBus accesses
+ #include <abstractions/dbus-strict>
+
+ # Allow access to fwupd service
+ dbus (receive, send)
+ bus=system
+ path=/
+ interface=org.freedesktop.fwupd
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+ dbus (receive, send)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.Properties
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`)
+
+var fwupdConnectedSlotAppArmor = []byte(`
+# Description: Allow firmware update using fwupd service. Reserved because this gives
+# privileged access to the fwupd service.
+# Usage: reserved
+
+ # Allow traffic to/from org.freedesktop.DBus for fwupd service
+ dbus (receive, send)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.**
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+ dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/fwupd{,/**}
+ interface=org.freedesktop.DBus.**
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+ # Allow traffic to/from fwupd interface with any method
+ dbus (receive, send)
+ bus=system
+ path=/
+ interface=org.freedesktop.fwupd
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+ dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/fwupd{,/**}
+ interface=org.freedesktop.fwupd
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var fwupdPermanentSlotDBus = []byte(`
+<policy user="root">
+ <allow own="org.freedesktop.fwupd"/>
+ <allow send_destination="org.freedesktop.fwupd" send_interface="org.freedesktop.fwupd"/>
+ <allow send_destination="org.freedesktop.fwupd" send_interface="org.freedesktop.DBus.Properties"/>
+ <allow send_destination="org.freedesktop.fwupd" send_interface="org.freedesktop.DBus.Introspectable"/>
+ <allow send_destination="org.freedesktop.fwupd" send_interface="org.freedesktop.DBus.Peer"/>
+</policy>
+<policy context="default">
+ <deny own="org.freedesktop.fwupd"/>
+ <deny send_destination="org.freedesktop.fwupd" send_interface="org.freedesktop.fwupd"/>
+</policy>
+`)
+
+var fwupdPermanentSlotSecComp = []byte(`
+# Description: Allow operating as the fwupd service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+# Can communicate with DBus system service
+bind
+getsockname
+recvfrom
+recvmsg
+sendmsg
+sendto
+setsockopt
+`)
+
+var fwupdConnectedPlugSecComp = []byte(`
+# Description: Allow using fwupd service. Reserved because this gives
+# privileged access to the fwupd service.
+# Usage: reserved
+bind
+getsockname
+getsockopt
+recvfrom
+recvmsg
+sendmsg
+sendto
+setsockopt
+`)
+
+// FwupdInterface type
+type FwupdInterface struct{}
+
+// Name of the FwupdInterface
+func (iface *FwupdInterface) Name() string {
+ return "fwupd"
+}
+
+// PermanentPlugSnippet - no slot snippets provided
+func (iface *FwupdInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// ConnectedPlugSnippet returns security snippets for plug at connection
+func (iface *FwupdInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(fwupdConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return fwupdConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+// PermanentSlotSnippet returns security snippets for slot at install
+func (iface *FwupdInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return fwupdPermanentSlotAppArmor, nil
+ case interfaces.SecurityDBus:
+ return fwupdPermanentSlotDBus, nil
+ case interfaces.SecuritySecComp:
+ return fwupdPermanentSlotSecComp, nil
+ }
+ return nil, nil
+}
+
+// ConnectedSlotSnippet returns security snippets for slot at connection
+func (iface *FwupdInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(fwupdConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+// SanitizePlug checks the plug definition is valid
+func (iface *FwupdInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+// SanitizeSlot checks the slot definition is valid
+func (iface *FwupdInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *FwupdInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type FwupdInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&FwupdInterfaceSuite{
+ iface: &builtin.FwupdInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "uefi-fw-tools"},
+ Name: "fwupd",
+ Interface: "fwupd",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "uefi-fw-tools"},
+ Name: "fwupdmgr",
+ Interface: "fwupd",
+ },
+ },
+})
+
+func (s *FwupdInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "fwupd")
+}
+
+// The label glob when all apps are bound to the fwupd slot
+func (s *FwupdInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "uefi-fw-tools",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "fwupd",
+ Interface: "fwupd",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.uefi-fw-tools.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the fwupd slot
+func (s *FwupdInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "uefi-fw-tools",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "fwupd",
+ Interface: "fwupd",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.uefi-fw-tools.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the fwupd slot
+func (s *FwupdInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "uefi-fw-tools",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "fwupd",
+ Interface: "fwupd",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.uefi-fw-tools.app"),`)
+}
+
+func (s *FwupdInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/systemd"
+)
+
+var gpioSysfsGpioBase = "/sys/class/gpio/gpio"
+var gpioSysfsExport = "/sys/class/gpio/export"
+
+// GpioInterface type
+type GpioInterface struct{}
+
+// String returns the same value as Name().
+func (iface *GpioInterface) String() string {
+ return iface.Name()
+}
+
+// Name of the GpioInterface
+func (iface *GpioInterface) Name() string {
+ return "gpio"
+}
+
+// SanitizeSlot checks the slot definition is valid
+func (iface *GpioInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Paranoid check this right interface type
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // We will only allow creation of this type of slot by a gadget or OS snap
+ if !(slot.Snap.Type == "gadget" || slot.Snap.Type == "os") {
+ return fmt.Errorf("gpio slots only allowed on gadget or core snaps")
+ }
+
+ // Must have a GPIO number
+ number, ok := slot.Attrs["number"]
+ if !ok {
+ return fmt.Errorf("gpio slot must have a number attribute")
+ }
+
+ // Valid values of number
+ if _, ok := number.(int64); !ok {
+ return fmt.Errorf("gpio slot number attribute must be an int")
+ }
+
+ // Slot is good
+ return nil
+}
+
+// SanitizePlug checks the plug definition is valid
+func (iface *GpioInterface) SanitizePlug(plug *interfaces.Plug) error {
+ // Make sure right interface type
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+
+ // Plug is good
+ return nil
+}
+
+// PermanentPlugSnippet returns security snippets for plug at install
+func (iface *GpioInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// ConnectedPlugSnippet returns security snippets for plug at connection
+func (iface *GpioInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ path := fmt.Sprint(gpioSysfsGpioBase, slot.Attrs["number"])
+ // Entries in /sys/class/gpio for single GPIO's are just symlinks
+ // to their correct device part in the sysfs tree. Given AppArmor
+ // requires symlinks to be dereferenced, evaluate the GPIO
+ // path and add the correct absolute path to the AppArmor snippet.
+ dereferencedPath, err := evalSymlinks(path)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(fmt.Sprintf("%s/* rwk,\n", dereferencedPath)), nil
+ }
+ return nil, nil
+}
+
+// PermanentSlotSnippet - no slot snippets provided
+func (iface *GpioInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *GpioInterface) ConnectedSlotRichSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) (*systemd.Snippet, error) {
+ switch securitySystem {
+ case interfaces.SecuritySystemd:
+ gpioNum, ok := slot.Attrs["number"].(int64)
+ if !ok {
+ return nil, fmt.Errorf("gpio slot has invalid number attribute: %q", slot.Attrs["number"])
+ }
+ serviceName := interfaces.InterfaceServiceName(slot.Snap.Name(), fmt.Sprintf("gpio-%d", gpioNum))
+ snippet := &systemd.Snippet{
+ Services: map[string]systemd.Service{
+ serviceName: {
+ Type: "oneshot",
+ RemainAfterExit: true,
+ ExecStart: fmt.Sprintf("/bin/sh -c 'test -e /sys/class/gpio/gpio%d || echo %d > /sys/class/gpio/export'", gpioNum, gpioNum),
+ ExecStop: fmt.Sprintf("/bin/sh -c 'test ! -e /sys/class/gpio/gpio%d || echo %d > /sys/class/gpio/unexport'", gpioNum, gpioNum),
+ },
+ },
+ }
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *GpioInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ richSnippet, err := iface.ConnectedSlotRichSnippet(plug, slot, securitySystem)
+ if err != nil {
+ return nil, err
+ }
+ if richSnippet == nil {
+ return nil, nil
+ }
+ rawSnippet, err := json.Marshal(richSnippet)
+ if err != nil {
+ return nil, err
+ }
+ return rawSnippet, nil
+}
+
+func (iface *GpioInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ "encoding/json"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type GpioInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+ gadgetGpioSlot *interfaces.Slot
+ gadgetMissingNumberSlot *interfaces.Slot
+ gadgetBadNumberSlot *interfaces.Slot
+ gadgetBadInterfaceSlot *interfaces.Slot
+ gadgetPlug *interfaces.Plug
+ gadgetBadInterfacePlug *interfaces.Plug
+ osGpioSlot *interfaces.Slot
+ appGpioSlot *interfaces.Slot
+}
+
+var _ = Suite(&GpioInterfaceSuite{
+ iface: &builtin.GpioInterface{},
+})
+
+func (s *GpioInterfaceSuite) SetUpTest(c *C) {
+ gadgetInfo := snaptest.MockInfo(c, `
+name: my-device
+type: gadget
+slots:
+ my-pin:
+ interface: gpio
+ number: 100
+ missing-number:
+ interface: gpio
+ bad-number:
+ interface: gpio
+ number: forty-two
+ bad-interface-slot: other-interface
+plugs:
+ plug: gpio
+ bad-interface-plug: other-interface
+`, nil)
+ s.gadgetGpioSlot = &interfaces.Slot{SlotInfo: gadgetInfo.Slots["my-pin"]}
+ s.gadgetMissingNumberSlot = &interfaces.Slot{SlotInfo: gadgetInfo.Slots["missing-number"]}
+ s.gadgetBadNumberSlot = &interfaces.Slot{SlotInfo: gadgetInfo.Slots["bad-number"]}
+ s.gadgetBadInterfaceSlot = &interfaces.Slot{SlotInfo: gadgetInfo.Slots["bad-interface-slot"]}
+ s.gadgetPlug = &interfaces.Plug{PlugInfo: gadgetInfo.Plugs["plug"]}
+ s.gadgetBadInterfacePlug = &interfaces.Plug{PlugInfo: gadgetInfo.Plugs["bad-interface-plug"]}
+
+ osInfo := snaptest.MockInfo(c, `
+name: my-core
+type: os
+slots:
+ my-pin:
+ interface: gpio
+ number: 777
+ direction: out
+`, nil)
+ s.osGpioSlot = &interfaces.Slot{SlotInfo: osInfo.Slots["my-pin"]}
+
+ appInfo := snaptest.MockInfo(c, `
+name: my-app
+slots:
+ my-pin:
+ interface: gpio
+ number: 154
+ direction: out
+`, nil)
+ s.appGpioSlot = &interfaces.Slot{SlotInfo: appInfo.Slots["my-pin"]}
+}
+
+func (s *GpioInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "gpio")
+}
+
+func (s *GpioInterfaceSuite) TestSanitizeSlotGadgetSnap(c *C) {
+ // gpio slot on gadget accepeted
+ err := s.iface.SanitizeSlot(s.gadgetGpioSlot)
+ c.Assert(err, IsNil)
+
+ // slots without number attribute are rejected
+ err = s.iface.SanitizeSlot(s.gadgetMissingNumberSlot)
+ c.Assert(err, ErrorMatches, "gpio slot must have a number attribute")
+
+ // slots with number attribute that isnt a number
+ err = s.iface.SanitizeSlot(s.gadgetBadNumberSlot)
+ c.Assert(err, ErrorMatches, "gpio slot number attribute must be an int")
+
+ // Must be right interface type
+ c.Assert(func() { s.iface.SanitizeSlot(s.gadgetBadInterfaceSlot) }, PanicMatches, `slot is not of interface "gpio"`)
+}
+
+func (s *GpioInterfaceSuite) TestSanitizeSlotOsSnap(c *C) {
+ // gpio slot on OS accepeted
+ err := s.iface.SanitizeSlot(s.osGpioSlot)
+ c.Assert(err, IsNil)
+}
+
+func (s *GpioInterfaceSuite) TestSanitizeSlotAppSnap(c *C) {
+ // gpio slot not accepted on app snap
+ err := s.iface.SanitizeSlot(s.appGpioSlot)
+ c.Assert(err, ErrorMatches, "gpio slots only allowed on gadget or core snaps")
+}
+
+func (s *GpioInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.gadgetPlug)
+ c.Assert(err, IsNil)
+
+ // It is impossible to use "bool-file" interface to sanitize plugs of different interface.
+ c.Assert(func() { s.iface.SanitizePlug(s.gadgetBadInterfacePlug) }, PanicMatches, `plug is not of interface "gpio"`)
+}
+
+func (s *GpioInterfaceSuite) TestConnectedSlotSnippet(c *C) {
+ snippet, err := s.iface.ConnectedSlotSnippet(s.gadgetPlug, s.gadgetGpioSlot, interfaces.SecuritySystemd)
+ c.Assert(err, IsNil)
+ var data interface{}
+ err = json.Unmarshal(snippet, &data)
+ c.Assert(err, IsNil)
+ c.Assert(data, DeepEquals, map[string]interface{}{
+ "services": map[string]interface{}{
+ "snap.my-device.interface.gpio-100.service": map[string]interface{}{
+ "type": "oneshot",
+ "remain-after-exit": true,
+ "exec-start": `/bin/sh -c 'test -e /sys/class/gpio/gpio100 || echo 100 > /sys/class/gpio/export'`,
+ "exec-stop": `/bin/sh -c 'test ! -e /sys/class/gpio/gpio100 || echo 100 > /sys/class/gpio/unexport'`,
+ },
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const gsettingsConnectedPlugAppArmor = `
+# Description: Can access global gsettings of the user's session. Restricted
+# because this gives privileged access to sensitive information stored in
+# gsettings and allows adjusting settings of other applications.
+# Usage: reserved
+
+#include <abstractions/dbus-session-strict>
+
+#include <abstractions/dconf>
+owner /{,var/}run/user/*/dconf/user w,
+owner @{HOME}/.config/dconf/user w,
+dbus (receive, send)
+ bus=session
+ interface="ca.desrt.dconf.Writer"
+ peer=(label=unconfined),
+`
+
+const gsettingsConnectedPlugSecComp = `
+# Description: Can access global gsettings of the user's session. Restricted
+# because this gives privileged access to sensitive information stored in
+# gsettings and allows adjusting settings of other applications.
+
+# dbus
+connect
+getsockname
+recvmsg
+send
+sendto
+sendmsg
+socket
+`
+
+// NewGsettingsInterface returns a new "gsettings" interface.
+func NewGsettingsInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "gsettings",
+ connectedPlugAppArmor: gsettingsConnectedPlugAppArmor,
+ connectedPlugSecComp: gsettingsConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type GsettingsInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&GsettingsInterfaceSuite{
+ iface: builtin.NewGsettingsInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "gsettings",
+ Interface: "gsettings",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "gsettings",
+ Interface: "gsettings",
+ },
+ },
+})
+
+func (s *GsettingsInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "gsettings")
+}
+
+func (s *GsettingsInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "gsettings",
+ Interface: "gsettings",
+ }})
+ c.Assert(err, ErrorMatches, "gsettings slots are reserved for the operating system snap")
+}
+
+func (s *GsettingsInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *GsettingsInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "gsettings"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "gsettings"`)
+}
+
+func (s *GsettingsInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/log-observe
+const hardwareObserveConnectedPlugAppArmor = `
+# Description: This interface allows for getting hardware information
+# from the system. this is reserved because it allows reading potentially sensitive information.
+# Usage: reserved
+
+# used by lscpu
+capability sys_rawio,
+
+# files in /sys pertaining to hardware
+/sys/{block,bus,class,devices,firmware}/{,**} r,
+
+# DMI tables
+/sys/firmware/dmi/tables/DMI r,
+/sys/firmware/dmi/tables/smbios_entry_point r,
+
+# Needed for udevadm
+/run/udev/data/** r,
+
+# util-linux
+/{,usr/}bin/lscpu ixr,
+@{PROC}/bus/pci/devices r,
+
+# lsusb
+# Note: lsusb and its database have to be shipped in the snap if not on classic
+/{,usr/}bin/lsusb ixr,
+/var/lib/usbutils/usb.ids r,
+/dev/ r,
+/dev/bus/usb/{,**/} r,
+/etc/udev/udev.conf r,
+`
+
+// NewHardwareObserveInterface returns a new "hardware-observe" interface.
+func NewHardwareObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "hardware-observe",
+ connectedPlugAppArmor: hardwareObserveConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type HardwareObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&HardwareObserveInterfaceSuite{
+ iface: builtin.NewHardwareObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "hardware-observe",
+ Interface: "hardware-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "hardware-observe",
+ Interface: "hardware-observe",
+ },
+ },
+})
+
+func (s *HardwareObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "hardware-observe")
+}
+
+func (s *HardwareObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "hardware-observe",
+ Interface: "hardware-observe",
+ }})
+ c.Assert(err, ErrorMatches, "hardware-observe slots are reserved for the operating system snap")
+}
+
+func (s *HardwareObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *HardwareObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "hardware-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "hardware-observe"`)
+}
+
+func (s *HardwareObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// HidrawInterface is the type for hidraw interfaces.
+type HidrawInterface struct{}
+
+// Name of the hidraw interface.
+func (iface *HidrawInterface) Name() string {
+ return "hidraw"
+}
+
+func (iface *HidrawInterface) String() string {
+ return iface.Name()
+}
+
+// Pattern to match allowed hidraw device nodes, path attributes will be
+// compared to this for validity when not using udev identification
+var hidrawDeviceNodePattern = regexp.MustCompile("^/dev/hidraw[0-9]{1,3}$")
+
+// Pattern that is considered valid for the udev symlink to the hidraw device,
+// path attributes will be compared to this for validity when usb vid and pid
+// are also specified
+var hidrawUdevSymlinkPattern = regexp.MustCompile("^/dev/hidraw-[a-z0-9]+$")
+
+// SanitizeSlot checks validity of the defined slot
+func (iface *HidrawInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Check slot is of right type
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // We will only allow creation of this type of slot by a gadget or OS snap
+ if !(slot.Snap.Type == "gadget" || slot.Snap.Type == "os") {
+ return fmt.Errorf("hidraw slots only allowed on gadget or core snaps")
+ }
+
+ // Check slot has a path attribute identify hidraw device
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return fmt.Errorf("hidraw slots must have a path attribute")
+ }
+
+ // Clean the path before further checks
+ path = filepath.Clean(path)
+
+ if iface.hasUsbAttrs(slot) {
+ // Must be path attribute where symlink will be placed and usb vendor and product identifiers
+ // Check the path attribute is in the allowable pattern
+ if !hidrawUdevSymlinkPattern.MatchString(path) {
+ return fmt.Errorf("hidraw path attribute specifies invalid symlink location")
+ }
+
+ usbVendor, vOk := slot.Attrs["usb-vendor"].(int64)
+ if !vOk {
+ return fmt.Errorf("hidraw slot failed to find usb-vendor attribute")
+ }
+ if (usbVendor < 0x1) || (usbVendor > 0xFFFF) {
+ return fmt.Errorf("hidraw usb-vendor attribute not valid: %d", usbVendor)
+ }
+
+ usbProduct, pOk := slot.Attrs["usb-product"].(int64)
+ if !pOk {
+ return fmt.Errorf("hidraw slot failed to find usb-product attribute")
+ }
+ if (usbProduct < 0x0) || (usbProduct > 0xFFFF) {
+ return fmt.Errorf("hidraw usb-product attribute not valid: %d", usbProduct)
+ }
+ } else {
+ // Just a path attribute - must be a valid usb device node
+ // Check the path attribute is in the allowable pattern
+ if !hidrawDeviceNodePattern.MatchString(path) {
+ return fmt.Errorf("hidraw path attribute must be a valid device node")
+ }
+ }
+ return nil
+}
+
+// SanitizePlug checks and possibly modifies a plug.
+func (iface *HidrawInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // NOTE: currently we don't check anything on the plug side.
+ return nil
+}
+
+// PermanentSlotSnippet returns snippets granted on install
+func (iface *HidrawInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityUDev:
+ usbVendor, vOk := slot.Attrs["usb-vendor"].(int64)
+ if !vOk {
+ return nil, nil
+ }
+ usbProduct, pOk := slot.Attrs["usb-product"].(int64)
+ if !pOk {
+ return nil, nil
+ }
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return nil, nil
+ }
+ return udevUsbDeviceSnippet("hidraw", usbVendor, usbProduct, "SYMLINK", strings.TrimPrefix(path, "/dev/")), nil
+ }
+ return nil, nil
+}
+
+// ConnectedSlotSnippet no extra permissions granted on connection
+func (iface *HidrawInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// PermanentPlugSnippet no permissions provided to plug permanently
+func (iface *HidrawInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// ConnectedPlugSnippet returns security snippet specific to the plug
+func (iface *HidrawInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ if iface.hasUsbAttrs(slot) {
+ // This apparmor rule must match hidrawDeviceNodePattern
+ // UDev tagging and device cgroups will restrict down to the specific device
+ return []byte("/dev/hidraw[0-9]{,[0-9],[0-9][0-9]} rw,\n"), nil
+ }
+
+ // Path to fixed device node (no udev tagging)
+ path, pathOk := slot.Attrs["path"].(string)
+ if !pathOk {
+ return nil, nil
+ }
+ cleanedPath := filepath.Clean(path)
+ return []byte(fmt.Sprintf("%s rw,\n", cleanedPath)), nil
+ case interfaces.SecurityUDev:
+ usbVendor, vOk := slot.Attrs["usb-vendor"].(int64)
+ if !vOk {
+ return nil, nil
+ }
+ usbProduct, pOk := slot.Attrs["usb-product"].(int64)
+ if !pOk {
+ return nil, nil
+ }
+ var udevSnippet bytes.Buffer
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ udevSnippet.Write(udevUsbDeviceSnippet("hidraw", usbVendor, usbProduct, "TAG", tag))
+ }
+ return udevSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+func (iface *HidrawInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
+
+func (iface *HidrawInterface) hasUsbAttrs(slot *interfaces.Slot) bool {
+ if _, ok := slot.Attrs["usb-vendor"]; ok {
+ return true
+ }
+ if _, ok := slot.Attrs["usb-product"]; ok {
+ return true
+ }
+ return false
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type HidrawInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+
+ // OS Snap
+ testSlot1 *interfaces.Slot
+ testSlot2 *interfaces.Slot
+ missingPathSlot *interfaces.Slot
+ badPathSlot1 *interfaces.Slot
+ badPathSlot2 *interfaces.Slot
+ badPathSlot3 *interfaces.Slot
+ badInterfaceSlot *interfaces.Slot
+
+ // Gadget Snap
+ testUdev1 *interfaces.Slot
+ testUdev2 *interfaces.Slot
+ testUdevBadValue1 *interfaces.Slot
+ testUdevBadValue2 *interfaces.Slot
+ testUdevBadValue3 *interfaces.Slot
+
+ // Consuming Snap
+ testPlugPort1 *interfaces.Plug
+ testPlugPort2 *interfaces.Plug
+}
+
+var _ = Suite(&HidrawInterfaceSuite{
+ iface: &builtin.HidrawInterface{},
+})
+
+func (s *HidrawInterfaceSuite) SetUpTest(c *C) {
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-port-1:
+ interface: hidraw
+ path: /dev/hidraw0
+ test-port-2:
+ interface: hidraw
+ path: /dev/hidraw987
+ missing-path: hidraw
+ bad-path-1:
+ interface: hidraw
+ path: path
+ bad-path-2:
+ interface: hidraw
+ path: /dev/hid0
+ bad-path-3:
+ interface: hidraw
+ path: /dev/hidraw9271
+ bad-interface: other-interface
+`, nil)
+ s.testSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-1"]}
+ s.testSlot2 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-2"]}
+ s.missingPathSlot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["missing-path"]}
+ s.badPathSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-1"]}
+ s.badPathSlot2 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-2"]}
+ s.badPathSlot3 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-3"]}
+ s.badInterfaceSlot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-interface"]}
+
+ gadgetSnapInfo := snaptest.MockInfo(c, `
+name: some-device
+type: gadget
+slots:
+ test-udev-1:
+ interface: hidraw
+ usb-vendor: 0x0001
+ usb-product: 0x0001
+ path: /dev/hidraw-canbus
+ test-udev-2:
+ interface: hidraw
+ usb-vendor: 0xffff
+ usb-product: 0xffff
+ path: /dev/hidraw-mydevice
+ test-udev-bad-value-1:
+ interface: hidraw
+ usb-vendor: -1
+ usb-product: 0xffff
+ path: /dev/hidraw-mydevice
+ test-udev-bad-value-2:
+ interface: hidraw
+ usb-vendor: 0x1234
+ usb-product: 0x10000
+ path: /dev/hidraw-mydevice
+ test-udev-bad-value-3:
+ interface: hidraw
+ usb-vendor: 0x789a
+ usb-product: 0x4321
+ path: /dev/my-device
+`, nil)
+ s.testUdev1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-1"]}
+ s.testUdev2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-2"]}
+ s.testUdevBadValue1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-1"]}
+ s.testUdevBadValue2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-2"]}
+ s.testUdevBadValue3 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-3"]}
+
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-device-1:
+ interface: hidraw
+ plug-for-device-2:
+ interface: hidraw
+
+apps:
+ app-accessing-1-device:
+ command: foo
+ plugs: [hidraw]
+ app-accessing-2-devices:
+ command: bar
+ plugs: [plug-for-device-1, plug-for-device-2]
+`, nil)
+ s.testPlugPort1 = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-device-1"]}
+ s.testPlugPort2 = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-device-2"]}
+}
+
+func (s *HidrawInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "hidraw")
+}
+
+func (s *HidrawInterfaceSuite) TestSanitizeCoreSnapSlots(c *C) {
+ for _, slot := range []*interfaces.Slot{s.testSlot1, s.testSlot2} {
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+ }
+}
+
+func (s *HidrawInterfaceSuite) TestSanitizeBadCoreSnapSlots(c *C) {
+ // Slots without the "path" attribute are rejected.
+ err := s.iface.SanitizeSlot(s.missingPathSlot)
+ c.Assert(err, ErrorMatches, `hidraw slots must have a path attribute`)
+
+ // Slots with incorrect value of the "path" attribute are rejected.
+ for _, slot := range []*interfaces.Slot{s.badPathSlot1, s.badPathSlot2, s.badPathSlot3} {
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, ErrorMatches, "hidraw path attribute must be a valid device node")
+ }
+
+ // It is impossible to use "bool-file" interface to sanitize slots with other interfaces.
+ c.Assert(func() { s.iface.SanitizeSlot(s.badInterfaceSlot) }, PanicMatches, `slot is not of interface "hidraw"`)
+}
+
+func (s *HidrawInterfaceSuite) TestSanitizeGadgetSnapSlots(c *C) {
+ err := s.iface.SanitizeSlot(s.testUdev1)
+ c.Assert(err, IsNil)
+
+ err = s.iface.SanitizeSlot(s.testUdev2)
+ c.Assert(err, IsNil)
+}
+
+func (s *HidrawInterfaceSuite) TestSanitizeBadGadgetSnapSlots(c *C) {
+ err := s.iface.SanitizeSlot(s.testUdevBadValue1)
+ c.Assert(err, ErrorMatches, "hidraw usb-vendor attribute not valid: -1")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue2)
+ c.Assert(err, ErrorMatches, "hidraw usb-product attribute not valid: 65536")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue3)
+ c.Assert(err, ErrorMatches, "hidraw path attribute specifies invalid symlink location")
+}
+
+func (s *HidrawInterfaceSuite) TestPermanentSlotUdevSnippets(c *C) {
+ for _, slot := range []*interfaces.Slot{s.testSlot1, s.testSlot2} {
+ snippet, err := s.iface.PermanentSlotSnippet(slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ }
+
+ expectedSnippet1 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="hidraw", SUBSYSTEMS=="usb", ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0001", SYMLINK+="hidraw-canbus"
+`)
+ snippet, err := s.iface.PermanentSlotSnippet(s.testUdev1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ expectedSnippet2 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="hidraw", SUBSYSTEMS=="usb", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="ffff", SYMLINK+="hidraw-mydevice"
+`)
+ snippet, err = s.iface.PermanentSlotSnippet(s.testUdev2, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+}
+
+func (s *HidrawInterfaceSuite) TestConnectedPlugUdevSnippets(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+
+ expectedSnippet1 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="hidraw", SUBSYSTEMS=="usb", ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0001", TAG+="snap_client-snap_app-accessing-2-devices"
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ expectedSnippet2 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="hidraw", SUBSYSTEMS=="usb", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="ffff", TAG+="snap_client-snap_app-accessing-2-devices"
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort2, s.testUdev2, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+}
+
+func (s *HidrawInterfaceSuite) TestConnectedPlugAppArmorSnippets(c *C) {
+ expectedSnippet1 := []byte(`/dev/hidraw0 rw,
+`)
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ expectedSnippet2 := []byte(`/dev/hidraw[0-9]{,[0-9],[0-9][0-9]} rw,
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+
+ expectedSnippet3 := []byte(`/dev/hidraw[0-9]{,[0-9],[0-9][0-9]} rw,
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort2, s.testUdev2, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet3, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet3, snippet))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/home
+const homeConnectedPlugAppArmor = `
+# Description: Can access non-hidden files in user's $HOME. This is restricted
+# because it gives file access to all of the user's $HOME.
+# Usage: reserved
+
+# Note, @{HOME} is the user's $HOME, not the snap's $HOME
+
+# Allow read access to toplevel $HOME for the user
+owner @{HOME}/ r,
+
+# Allow read/write access to all files in @{HOME}, except snap application
+# data in @{HOME}/snaps and toplevel hidden directories in @{HOME}.
+owner @{HOME}/[^s.]** rwk,
+owner @{HOME}/s[^n]** rwk,
+owner @{HOME}/sn[^a]** rwk,
+owner @{HOME}/sna[^p]** rwk,
+# Allow creating a few files not caught above
+owner @{HOME}/{s,sn,sna}{,/} rwk,
+
+# Allow access to gvfs mounts for files owned by the user (including hidden
+# files; only allow writes to files, not the mount point).
+owner /run/user/[0-9]*/gvfs/{,**} r,
+owner /run/user/[0-9]*/gvfs/*/** w,
+`
+
+// NewHomeInterface returns a new "home" interface.
+func NewHomeInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "home",
+ connectedPlugAppArmor: homeConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type HomeInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&HomeInterfaceSuite{
+ iface: builtin.NewHomeInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "home",
+ Interface: "home",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "home",
+ Interface: "home",
+ },
+ },
+})
+
+func (s *HomeInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "home")
+}
+
+func (s *HomeInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "home",
+ Interface: "home",
+ }})
+ c.Assert(err, ErrorMatches, "home slots are reserved for the operating system snap")
+}
+
+func (s *HomeInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *HomeInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "home"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "home"`)
+}
+
+func (s *HomeInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/snapcore/snapd/interfaces"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// The type for i2c interface
+type I2cInterface struct{}
+
+// Getter for the name of the i2c interface
+func (iface *I2cInterface) Name() string {
+ return "i2c"
+}
+
+func (iface *I2cInterface) String() string {
+ return iface.Name()
+}
+
+// Pattern to match allowed i2c device nodes. It is gonna be used to check the
+// validity of the path attributes in case the udev is not used for
+// identification
+var i2cControlDeviceNodePattern = regexp.MustCompile("^/dev/i2c-[0-9]+$")
+
+// Check validity of the defined slot
+func (iface *I2cInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Does it have right type?
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // Creation of the slot of this type
+ // is allowed only by a gadget snap
+ if !(slot.Snap.Type == "gadget" || slot.Snap.Type == "os") {
+ return fmt.Errorf("%s slots only allowed on gadget or core snaps", iface.Name())
+ }
+
+ // Validate the path
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return fmt.Errorf("%s slot must have a path attribute", iface.Name())
+ }
+
+ path = filepath.Clean(path)
+
+ if !i2cControlDeviceNodePattern.MatchString(path) {
+ return fmt.Errorf("%s path attribute must be a valid device node", iface.Name())
+ }
+
+ return nil
+}
+
+// Checks and possibly modifies a plug
+func (iface *I2cInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // Currently nothing is checked on the plug side
+ return nil
+}
+
+// Returns snippet granted on install
+func (iface *I2cInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// Getter for the security snippet specific to the plug
+func (iface *I2cInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ path, pathOk := slot.Attrs["path"].(string)
+ if !pathOk {
+ return nil, nil
+ }
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ cleanedPath := filepath.Clean(path)
+ return []byte(fmt.Sprintf("%s rw,\n", cleanedPath)), nil
+
+ case interfaces.SecurityUDev:
+ var tagSnippet bytes.Buffer
+ const pathPrefix = "/dev/"
+ const udevRule string = `KERNEL=="%s", TAG+="%s"`
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ tagSnippet.WriteString(fmt.Sprintf(udevRule, strings.TrimPrefix(path, pathPrefix), tag))
+ tagSnippet.WriteString("\n")
+ }
+ return tagSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+// No extra permissions granted on connection
+func (iface *I2cInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// No permissions granted to plug permanently
+func (iface *I2cInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *I2cInterface) LegacyAutoConnect() bool {
+ return false
+}
+
+func (iface *I2cInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // Allow what is allowed in the declarations
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type I2cInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+
+ // OS Snap
+ testSlot1 *interfaces.Slot
+
+ // Gadget Snap
+ testUdev1 *interfaces.Slot
+ testUdev2 *interfaces.Slot
+ testUdev3 *interfaces.Slot
+ testUdevBadValue1 *interfaces.Slot
+ testUdevBadValue2 *interfaces.Slot
+ testUdevBadValue3 *interfaces.Slot
+ testUdevBadValue4 *interfaces.Slot
+ testUdevBadValue5 *interfaces.Slot
+ testUdevBadValue6 *interfaces.Slot
+ testUdevBadValue7 *interfaces.Slot
+ testUdevBadInterface1 *interfaces.Slot
+
+ // Consuming Snap
+ testPlugPort1 *interfaces.Plug
+}
+
+var _ = Suite(&I2cInterfaceSuite{
+ iface: &builtin.I2cInterface{},
+})
+
+func (s *I2cInterfaceSuite) SetUpTest(c *C) {
+ // Mock for OS Snap
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-port-1:
+ interface: i2c
+ path: /dev/i2c-0
+`, nil)
+ s.testSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-1"]}
+
+ // Mock for Gadget Snap
+ gadgetSnapInfo := snaptest.MockInfo(c, `
+name: some-device
+type: gadget
+slots:
+ test-udev-1:
+ interface: i2c
+ path: /dev/i2c-1
+ test-udev-2:
+ interface: i2c
+ path: /dev/i2c-11
+ test-udev-3:
+ interface: i2c
+ path: /dev/i2c-0
+ test-udev-bad-value-1:
+ interface: i2c
+ path: /dev/i2c
+ test-udev-bad-value-2:
+ interface: i2c
+ path: /dev/i2c-a
+ test-udev-bad-value-3:
+ interface: i2c
+ path: /dev/i2c-2a
+ test-udev-bad-value-4:
+ interface: i2c
+ path: /dev/foo-0
+ test-udev-bad-value-5:
+ interface: i2c
+ path: /dev/i2c-foo
+ test-udev-bad-value-6:
+ interface: i2c
+ path: ""
+ test-udev-bad-value-7:
+ interface: i2c
+ test-udev-bad-interface-1:
+ interface: other-interface
+`, nil)
+ s.testUdev1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-1"]}
+ s.testUdev2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-2"]}
+ s.testUdev3 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-3"]}
+ s.testUdevBadValue1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-1"]}
+ s.testUdevBadValue2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-2"]}
+ s.testUdevBadValue3 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-3"]}
+ s.testUdevBadValue4 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-4"]}
+ s.testUdevBadValue5 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-5"]}
+ s.testUdevBadValue6 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-6"]}
+ s.testUdevBadValue7 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-7"]}
+ s.testUdevBadInterface1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-interface-1"]}
+
+ // Snap Consumers
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-port-1:
+ interface: i2c
+apps:
+ app-accessing-1-port:
+ command: foo
+ plugs: [i2c]
+`, nil)
+ s.testPlugPort1 = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-port-1"]}
+}
+
+func (s *I2cInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "i2c")
+}
+
+func (s *I2cInterfaceSuite) TestSanitizeCoreSnapSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.testSlot1)
+ c.Assert(err, IsNil)
+}
+
+func (s *I2cInterfaceSuite) TestSanitizeGadgetSnapSlot(c *C) {
+
+ err := s.iface.SanitizeSlot(s.testUdev1)
+ c.Assert(err, IsNil)
+
+ err = s.iface.SanitizeSlot(s.testUdev2)
+ c.Assert(err, IsNil)
+
+ err = s.iface.SanitizeSlot(s.testUdev3)
+ c.Assert(err, IsNil)
+}
+
+func (s *I2cInterfaceSuite) TestSanitizeBadGadgetSnapSlot(c *C) {
+
+ err := s.iface.SanitizeSlot(s.testUdevBadValue1)
+ c.Assert(err, ErrorMatches, "i2c path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue2)
+ c.Assert(err, ErrorMatches, "i2c path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue3)
+ c.Assert(err, ErrorMatches, "i2c path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue4)
+ c.Assert(err, ErrorMatches, "i2c path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue5)
+ c.Assert(err, ErrorMatches, "i2c path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue6)
+ c.Assert(err, ErrorMatches, "i2c slot must have a path attribute")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue7)
+ c.Assert(err, ErrorMatches, "i2c slot must have a path attribute")
+
+ c.Assert(func() { s.iface.SanitizeSlot(s.testUdevBadInterface1) }, PanicMatches, `slot is not of interface "i2c"`)
+}
+
+func (s *I2cInterfaceSuite) TestConnectedPlugUdevSnippets(c *C) {
+ expectedSnippet1 := []byte(`KERNEL=="i2c-1", TAG+="snap_client-snap_app-accessing-1-port"
+`)
+
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+}
+
+func (s *I2cInterfaceSuite) TestConnectedPlugAppArmorSnippets(c *C) {
+ expectedSnippet1 := []byte(`/dev/i2c-1 rw,
+`)
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+}
+
+func (s *I2cInterfaceSuite) TestAutoConnect(c *C) {
+ c.Check(s.iface.AutoConnect(nil, nil), Equals, true)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var iioConnectedPlugAppArmor = []byte(`
+# Description: Give access to a specific IIO device on the system.
+
+###IIO_DEVICE_PATH### rw,
+/sys/bus/iio/devices/###IIO_DEVICE_NAME###/ r,
+/sys/bus/iio/devices/###IIO_DEVICE_NAME###/** rwk,
+`)
+
+// The type for iio interface
+type IioInterface struct{}
+
+// Getter for the name of the iio interface
+func (iface *IioInterface) Name() string {
+ return "iio"
+}
+
+func (iface *IioInterface) String() string {
+ return iface.Name()
+}
+
+// Pattern to match allowed iio device nodes. It is going to be used to check the
+// validity of the path attributes in case the udev is not used for
+// identification
+var iioControlDeviceNodePattern = regexp.MustCompile("^/dev/iio:device[0-9]+$")
+
+// Check validity of the defined slot
+func (iface *IioInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Does it have right type?
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // Creation of the slot of this type
+ // is allowed only by a gadget or os snap
+ if !(slot.Snap.Type == "gadget" || slot.Snap.Type == "os") {
+ return fmt.Errorf("%s slots only allowed on gadget or core snaps", iface.Name())
+ }
+
+ // Validate the path
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return fmt.Errorf("%s slot must have a path attribute", iface.Name())
+ }
+
+ path = filepath.Clean(path)
+
+ if !iioControlDeviceNodePattern.MatchString(path) {
+ return fmt.Errorf("%s path attribute must be a valid device node", iface.Name())
+ }
+
+ return nil
+}
+
+// Checks and possibly modifies a plug
+func (iface *IioInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // Currently nothing is checked on the plug side
+ return nil
+}
+
+// Returns snippet granted on install
+func (iface *IioInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// Getter for the security snippet specific to the plug
+func (iface *IioInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ path, pathOk := slot.Attrs["path"].(string)
+ if !pathOk {
+ return nil, nil
+ }
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ cleanedPath := filepath.Clean(path)
+ snippet := bytes.Replace(iioConnectedPlugAppArmor, []byte("###IIO_DEVICE_PATH###"), []byte(cleanedPath), -1)
+
+ // The path is already verified against a regular expression
+ // in SanitizeSlot so we can rely on its structure here and
+ // safely strip the '/dev/' prefix to get the actual name of
+ // the IIO device.
+ deviceName := strings.TrimPrefix(path, "/dev/")
+ snippet = bytes.Replace(snippet, []byte("###IIO_DEVICE_NAME###"), []byte(deviceName), -1)
+
+ return snippet, nil
+
+ case interfaces.SecurityUDev:
+ var tagSnippet bytes.Buffer
+ const pathPrefix = "/dev/"
+ const udevRule = `KERNEL=="%s", TAG+="%s"`
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ tagSnippet.WriteString(fmt.Sprintf(udevRule, strings.TrimPrefix(path, pathPrefix), tag))
+ tagSnippet.WriteString("\n")
+ }
+ return tagSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+// No extra permissions granted on connection
+func (iface *IioInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// No permissions granted to plug permanently
+func (iface *IioInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *IioInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // Allow what is allowed in the declarations
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type IioInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+
+ // OS Snap
+ testSlot1 *interfaces.Slot
+
+ // Gadget Snap
+ testUdev1 *interfaces.Slot
+ testUdev2 *interfaces.Slot
+ testUdev3 *interfaces.Slot
+ testUdevBadValue1 *interfaces.Slot
+ testUdevBadValue2 *interfaces.Slot
+ testUdevBadValue3 *interfaces.Slot
+ testUdevBadValue4 *interfaces.Slot
+ testUdevBadValue5 *interfaces.Slot
+ testUdevBadValue6 *interfaces.Slot
+ testUdevBadValue7 *interfaces.Slot
+ testUdevBadValue8 *interfaces.Slot
+ testUdevBadInterface1 *interfaces.Slot
+
+ // Consuming Snap
+ testPlugPort1 *interfaces.Plug
+}
+
+var _ = Suite(&IioInterfaceSuite{
+ iface: &builtin.IioInterface{},
+})
+
+func (s *IioInterfaceSuite) SetUpTest(c *C) {
+ // Mock for OS Snap
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-port-1:
+ interface: iio
+ path: /dev/iio:device0
+`, nil)
+ s.testSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-1"]}
+
+ // Mock for Gadget Snap
+ gadgetSnapInfo := snaptest.MockInfo(c, `
+name: some-device
+type: gadget
+slots:
+ test-udev-1:
+ interface: iio
+ path: /dev/iio:device1
+ test-udev-2:
+ interface: iio
+ path: /dev/iio:device2
+ test-udev-3:
+ interface: iio
+ path: /dev/iio:device10000
+ test-udev-bad-value-1:
+ interface: iio
+ path: /dev/iio
+ test-udev-bad-value-2:
+ interface: iio
+ path: /dev/iio:devicea
+ test-udev-bad-value-3:
+ interface: iio
+ path: /dev/iio:device2a
+ test-udev-bad-value-4:
+ interface: iio
+ path: /dev/foo-0
+ test-udev-bad-value-5:
+ interface: iio
+ path: /dev/iio:devicefoo
+ test-udev-bad-value-6:
+ interface: iio
+ path: /dev/iio-device0
+ test-udev-bad-value-7:
+ interface: iio
+ path: ""
+ test-udev-bad-value-8:
+ interface: iio
+ test-udev-bad-interface-1:
+ interface: other-interface
+`, nil)
+ s.testUdev1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-1"]}
+ s.testUdev2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-2"]}
+ s.testUdev3 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-3"]}
+ s.testUdevBadValue1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-1"]}
+ s.testUdevBadValue2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-2"]}
+ s.testUdevBadValue3 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-3"]}
+ s.testUdevBadValue4 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-4"]}
+ s.testUdevBadValue5 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-5"]}
+ s.testUdevBadValue6 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-6"]}
+ s.testUdevBadValue7 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-7"]}
+ s.testUdevBadValue8 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-8"]}
+ s.testUdevBadInterface1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-interface-1"]}
+
+ // Snap Consumers
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-port-1:
+ interface: iio
+apps:
+ app-accessing-1-port:
+ command: foo
+ plugs: [iio]
+`, nil)
+ s.testPlugPort1 = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-port-1"]}
+}
+
+func (s *IioInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "iio")
+}
+
+func (s *IioInterfaceSuite) TestSanitizeBadGadgetSnapSlot(c *C) {
+
+ err := s.iface.SanitizeSlot(s.testUdevBadValue1)
+ c.Assert(err, ErrorMatches, "iio path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue2)
+ c.Assert(err, ErrorMatches, "iio path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue3)
+ c.Assert(err, ErrorMatches, "iio path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue4)
+ c.Assert(err, ErrorMatches, "iio path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue5)
+ c.Assert(err, ErrorMatches, "iio path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue6)
+ c.Assert(err, ErrorMatches, "iio path attribute must be a valid device node")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue7)
+ c.Assert(err, ErrorMatches, "iio slot must have a path attribute")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue8)
+ c.Assert(err, ErrorMatches, "iio slot must have a path attribute")
+
+ c.Assert(func() { s.iface.SanitizeSlot(s.testUdevBadInterface1) }, PanicMatches, `slot is not of interface "iio"`)
+}
+
+func (s *IioInterfaceSuite) TestConnectedPlugUdevSnippets(c *C) {
+ expectedSnippet1 := []byte(`KERNEL=="iio:device1", TAG+="snap_client-snap_app-accessing-1-port"
+`)
+
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+}
+
+func (s *IioInterfaceSuite) TestConnectedPlugAppArmorSnippets(c *C) {
+ expectedSnippet1 := []byte(`
+# Description: Give access to a specific IIO device on the system.
+
+/dev/iio:device1 rw,
+/sys/bus/iio/devices/iio:device1/ r,
+/sys/bus/iio/devices/iio:device1/** rwk,
+`)
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+}
+
+func (s *IioInterfaceSuite) TestAutoConnect(c *C) {
+ c.Check(s.iface.AutoConnect(nil, nil), Equals, true)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const ioPortsControlConnectedPlugAppArmor = `
+# Description: Allow write access to all I/O ports.
+# See 'man 4 mem' for details.
+
+capability sys_rawio, # required by iopl
+
+/dev/ports rw,
+`
+
+const ioPortsControlConnectedPlugSecComp = `
+# Description: Allow changes to the I/O port permissions and
+# privilege level of the calling process. In addition to granting
+# unrestricted I/O port access, running at a higher I/O privilege
+# level also allows the process to disable interrupts. This will
+# probably crash the system, and is not recommended.
+ioperm
+iopl
+`
+
+// The type for io-ports-control interface
+type IioPortsControlInterface struct{}
+
+// Getter for the name of the io-ports-control interface
+func (iface *IioPortsControlInterface) Name() string {
+ return "io-ports-control"
+}
+
+func (iface *IioPortsControlInterface) String() string {
+ return iface.Name()
+}
+
+// Check validity of the defined slot
+func (iface *IioPortsControlInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Does it have right type?
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // Creation of the slot of this type
+ // is allowed only by a gadget or os snap
+ if !(slot.Snap.Type == "os") {
+ return fmt.Errorf("%s slots only allowed on core snap", iface.Name())
+ }
+ return nil
+}
+
+// Checks and possibly modifies a plug
+func (iface *IioPortsControlInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // Currently nothing is checked on the plug side
+ return nil
+}
+
+// Returns snippet granted on install
+func (iface *IioPortsControlInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// Getter for the security snippet specific to the plug
+func (iface *IioPortsControlInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(ioPortsControlConnectedPlugAppArmor), nil
+
+ case interfaces.SecuritySecComp:
+ return []byte(ioPortsControlConnectedPlugSecComp), nil
+
+ case interfaces.SecurityUDev:
+ var tagSnippet bytes.Buffer
+ const udevRule = `KERNEL=="ports", TAG+="%s"`
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ tagSnippet.WriteString(fmt.Sprintf(udevRule, tag))
+ tagSnippet.WriteString("\n")
+ }
+ return tagSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+// No extra permissions granted on connection
+func (iface *IioPortsControlInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// No permissions granted to plug permanently
+func (iface *IioPortsControlInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *IioPortsControlInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // Allow what is allowed in the declarations
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type IioPortsControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&IioPortsControlInterfaceSuite{
+ iface: &builtin.IioPortsControlInterface{},
+})
+
+func (s *IioPortsControlInterfaceSuite) SetUpTest(c *C) {
+ // Mock for OS Snap
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-io-ports:
+ interface: io-ports-control
+`, nil)
+ s.slot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-io-ports"]}
+
+ // Snap Consumers
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-io-ports:
+ interface: io-ports-control
+apps:
+ app-accessing-io-ports:
+ command: foo
+ plugs: [plug-for-io-ports]
+`, nil)
+ s.plug = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-io-ports"]}
+}
+
+func (s *IioPortsControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "io-ports-control")
+}
+
+func (s *IioPortsControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "io-ports-control",
+ Interface: "io-ports-control",
+ }})
+ c.Assert(err, ErrorMatches, "io-ports-control slots only allowed on core snap")
+}
+
+func (s *IioPortsControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *IioPortsControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "io-ports-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "io-ports-control"`)
+}
+
+func (s *IioPortsControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ expectedSnippet1 := []byte(`
+# Description: Allow write access to all I/O ports.
+# See 'man 4 mem' for details.
+
+capability sys_rawio, # required by iopl
+
+/dev/ports rw,
+`)
+
+ expectedSnippet2 := []byte(`
+# Description: Allow changes to the I/O port permissions and
+# privilege level of the calling process. In addition to granting
+# unrestricted I/O port access, running at a higher I/O privilege
+# level also allows the process to disable interrupts. This will
+# probably crash the system, and is not recommended.
+ioperm
+iopl
+`)
+
+ expectedSnippet3 := []byte(`KERNEL=="ports", TAG+="snap_client-snap_app-accessing-io-ports"
+`)
+
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet3, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet3, snippet))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const kernelModuleControlConnectedPlugAppArmor = `
+# Description: Allow insertion, removal and querying of modules.
+
+ capability sys_module,
+ @{PROC}/modules r,
+
+ # NOTE: needed by lscpu. In the future this may be moved to system-trace or
+ # system-observe.
+ /dev/mem r,
+`
+
+const kernelModuleControlConnectedPlugSecComp = `
+# Description: Allow insertion, removal and querying of modules.
+
+init_module
+finit_module
+delete_module
+`
+
+// NewKernelModuleControlInterface returns a new "kernel-module" interface.
+func NewKernelModuleControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "kernel-module-control",
+ connectedPlugAppArmor: kernelModuleControlConnectedPlugAppArmor,
+ connectedPlugSecComp: kernelModuleControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type KernelModuleControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&KernelModuleControlInterfaceSuite{
+ iface: builtin.NewKernelModuleControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "kernel-module-control",
+ Interface: "kernel-module-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "kernel-module-control",
+ Interface: "kernel-module-control",
+ },
+ },
+})
+
+func (s *KernelModuleControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "kernel-module-control")
+}
+
+func (s *KernelModuleControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "kernel-module-control",
+ Interface: "kernel-module-control",
+ }})
+ c.Assert(err, ErrorMatches, "kernel-module-control slots are reserved for the operating system snap")
+}
+
+func (s *KernelModuleControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *KernelModuleControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "kernel-module-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "kernel-module-control"`)
+}
+
+func (s *KernelModuleControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const libvirtConnectedPlugAppArmor = `
+/run/libvirt/libvirt-sock rw,
+/etc/libvirt/* r,
+`
+
+const libvirtConnectedPlugSecComp = `
+connect
+getsockname
+recv
+recvmsg
+send
+sendto
+sendmsg
+socket
+socketpair
+listen
+accept
+`
+
+func NewLibvirtInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "libvirt",
+ connectedPlugAppArmor: libvirtConnectedPlugAppArmor,
+ connectedPlugSecComp: libvirtConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type LibvirtInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LibvirtInterfaceSuite{
+ iface: builtin.NewLibvirtInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "libvirt"},
+ Name: "libvirt",
+ Interface: "libvirt",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "libvirt",
+ Interface: "libvirt",
+ },
+ },
+})
+
+func (s *LibvirtInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "libvirt")
+}
+
+func (s *LibvirtInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, ErrorMatches, ".*libvirt slots are reserved for the operating system snap.*")
+}
+
+func (s *LibvirtInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/locale-control
+const localeControlConnectedPlugAppArmor = `
+# Description: Can manage locales directly separate from 'config ubuntu-core'.
+# Usage: reserved
+
+# TODO: this won't work until snappy exposes this configurability
+/etc/default/locale rw,
+`
+
+// NewLocaleControlInterface returns a new "locale-control" interface.
+func NewLocaleControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "locale-control",
+ connectedPlugAppArmor: localeControlConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type LocaleControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LocaleControlInterfaceSuite{
+ iface: builtin.NewLocaleControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "locale-control",
+ Interface: "locale-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "locale-control",
+ Interface: "locale-control",
+ },
+ },
+})
+
+func (s *LocaleControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "locale-control")
+}
+
+func (s *LocaleControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "locale-control",
+ Interface: "locale-control",
+ }})
+ c.Assert(err, ErrorMatches, "locale-control slots are reserved for the operating system snap")
+}
+
+func (s *LocaleControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *LocaleControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "locale-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "locale-control"`)
+}
+
+func (s *LocaleControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var locationControlPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the location service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+# DBus accesses
+#include <abstractions/dbus-strict>
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="{Request,Release}Name"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionUnix{ProcessID,User}"
+ peer=(label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="com.ubuntu.location.Service",
+
+dbus (receive, send)
+ bus=system
+ path=/com/ubuntu/location/Service{,/**}
+ interface=org.freedesktop.DBus**
+ peer=(label=unconfined),
+`)
+
+var locationControlConnectedSlotAppArmor = []byte(`
+# Allow connected clients to interact with the service
+
+# Allow clients to register providers
+dbus (receive)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=com.ubuntu.location.Service
+ member="AddProvider"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow clients to query/modify service properties
+dbus (receive)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member="{Get,Set}"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var locationControlConnectedPlugAppArmor = []byte(`
+# Description: Allow using location service. Reserved because this gives
+# privileged access to the service.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Allow clients to register providers
+dbus (send)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=com.ubuntu.location.Service
+ member="AddProvider"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Allow clients to query service properties
+dbus (send)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member="{Get,Set}"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=unconfined),
+`)
+
+var locationControlPermanentSlotSecComp = []byte(`
+getsockname
+recvmsg
+sendmsg
+sendto
+`)
+
+var locationControlConnectedPlugSecComp = []byte(`
+getsockname
+recvmsg
+sendmsg
+sendto
+`)
+
+var locationControlPermanentSlotDBus = []byte(`
+<policy user="root">
+ <allow own="com.ubuntu.location.Service"/>
+ <allow send_destination="com.ubuntu.location.Service"/>
+ <allow send_interface="com.ubuntu.location.Service"/>
+</policy>
+`)
+
+var locationControlConnectedPlugDBus = []byte(`
+<policy context="default">
+ <deny own="com.ubuntu.location.Service"/>
+ <allow send_destination="com.ubuntu.location.Service"/>
+ <allow send_interface="com.ubuntu.location.Service"/>
+</policy>
+`)
+
+type LocationControlInterface struct{}
+
+func (iface *LocationControlInterface) Name() string {
+ return "location-control"
+}
+
+func (iface *LocationControlInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LocationControlInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(locationControlConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecurityDBus:
+ return locationControlConnectedPlugDBus, nil
+ case interfaces.SecuritySecComp:
+ return locationControlConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *LocationControlInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return locationControlPermanentSlotAppArmor, nil
+ case interfaces.SecurityDBus:
+ return locationControlPermanentSlotDBus, nil
+ case interfaces.SecuritySecComp:
+ return locationControlPermanentSlotSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *LocationControlInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(locationControlConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *LocationControlInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *LocationControlInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *LocationControlInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type LocationControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LocationControlInterfaceSuite{
+ iface: &builtin.LocationControlInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "location"},
+ Name: "location",
+ Interface: "location-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "location"},
+ Name: "location-client",
+ Interface: "location-control",
+ },
+ },
+})
+
+func (s *LocationControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "location-control")
+}
+
+// The label glob when all apps are bound to the location slot
+func (s *LocationControlInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the location slot
+func (s *LocationControlInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the location slot
+func (s *LocationControlInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.app"),`)
+}
+
+// The label glob when all apps are bound to the location plug
+func (s *LocationControlInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the location plug
+func (s *LocationControlInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the location plug
+func (s *LocationControlInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.app"),`)
+}
+
+func (s *LocationControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp, interfaces.SecurityDBus}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var locationObservePermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the location service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+# DBus accesses
+#include <abstractions/dbus-strict>
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="{Request,Release}Name"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionUnix{ProcessID,User}"
+ peer=(label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="com.ubuntu.location.Service",
+
+dbus (receive, send)
+ bus=system
+ path=/com/ubuntu/location/Service{,/**}
+ interface=org.freedesktop.DBus**
+ peer=(label=unconfined),
+`)
+
+var locationObserveConnectedSlotAppArmor = []byte(`
+# Allow connected clients to interact with the service
+
+# Allow the service to host sessions
+dbus (bind)
+ bus=system
+ name="com.ubuntu.location.Service.Session",
+
+# Allow clients to create a session
+dbus (receive)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=com.ubuntu.location.Service
+ member=CreateSessionForCriteria
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow clients to query service properties
+dbus (receive)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member=Get
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow clients to request starting/stopping updates
+dbus (receive)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="{Start,Stop}PositionUpdates"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="{Start,Stop}HeadingUpdates"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="{Start,Stop}VelocityUpdates"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow the service to send updates to clients
+dbus (send)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="Update{Position,Heading,Velocity}"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var locationObserveConnectedPlugAppArmor = []byte(`
+# Description: Allow using location service. Reserved because this gives
+# privileged access to the service.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Allow clients to query service properties
+dbus (send)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member=Get
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Allow clients to create a session
+dbus (send)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=com.ubuntu.location.Service
+ member=CreateSessionForCriteria
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Allow clients to request starting/stopping updates
+dbus (send)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="{Start,Stop}PositionUpdates"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="{Start,Stop}HeadingUpdates"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="{Start,Stop}VelocityUpdates"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Allow clients to receive updates from the service
+dbus (receive)
+ bus=system
+ path=/sessions/*
+ interface=com.ubuntu.location.Service.Session
+ member="Update{Position,Heading,Velocity}"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=system
+ path=/com/ubuntu/location/Service
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=system
+ path=/
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=unconfined),
+`)
+
+var locationObservePermanentSlotSecComp = []byte(`
+getsockname
+recvmsg
+sendmsg
+sendto
+`)
+
+var locationObserveConnectedPlugSecComp = []byte(`
+getsockname
+recvmsg
+sendmsg
+sendto
+`)
+
+var locationObservePermanentSlotDBus = []byte(`
+<policy user="root">
+ <allow own="com.ubuntu.location.Service"/>
+ <allow own="com.ubuntu.location.Service.Session"/>
+ <allow send_destination="com.ubuntu.location.Service"/>
+ <allow send_destination="com.ubuntu.location.Service.Session"/>
+ <allow send_interface="com.ubuntu.location.Service"/>
+ <allow send_interface="com.ubuntu.location.Service.Session"/>
+</policy>
+`)
+
+var locationObserveConnectedPlugDBus = []byte(`
+<policy context="default">
+ <deny own="com.ubuntu.location.Service"/>
+ <allow send_destination="com.ubuntu.location.Service"/>
+ <allow send_destination="com.ubuntu.location.Service.Session"/>
+ <allow send_interface="com.ubuntu.location.Service"/>
+ <allow send_interface="com.ubuntu.location.Service.Session"/>
+</policy>
+`)
+
+type LocationObserveInterface struct{}
+
+func (iface *LocationObserveInterface) Name() string {
+ return "location-observe"
+}
+
+func (iface *LocationObserveInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LocationObserveInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(locationObserveConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecurityDBus:
+ return locationObserveConnectedPlugDBus, nil
+ case interfaces.SecuritySecComp:
+ return locationObserveConnectedPlugSecComp, nil
+ default:
+ return nil, nil
+ }
+}
+
+func (iface *LocationObserveInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return locationObservePermanentSlotAppArmor, nil
+ case interfaces.SecurityDBus:
+ return locationObservePermanentSlotDBus, nil
+ case interfaces.SecuritySecComp:
+ return locationObservePermanentSlotSecComp, nil
+ default:
+ return nil, nil
+ }
+}
+
+func (iface *LocationObserveInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(locationObserveConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ default:
+ return nil, nil
+ }
+}
+
+func (iface *LocationObserveInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *LocationObserveInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *LocationObserveInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type LocationObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LocationObserveInterfaceSuite{
+ iface: &builtin.LocationObserveInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "location"},
+ Name: "location",
+ Interface: "location-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "location"},
+ Name: "location-client",
+ Interface: "location-observe",
+ },
+ },
+})
+
+func (s *LocationObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "location-observe")
+}
+
+// The label glob when all apps are bound to the location slot
+func (s *LocationObserveInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the location slot
+func (s *LocationObserveInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the location slot
+func (s *LocationObserveInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.app"),`)
+}
+
+// The label glob when all apps are bound to the location plug
+func (s *LocationObserveInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the location plug
+func (s *LocationObserveInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the location plug
+func (s *LocationObserveInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "location",
+ Interface: "location",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.location.app"),`)
+}
+
+func (s *LocationObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp, interfaces.SecurityDBus}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/log-observe
+const logObserveConnectedPlugAppArmor = `
+# Description: Can read system logs and set kernel log rate-limiting
+# Usage: reserved
+
+/var/log/ r,
+/var/log/** r,
+/run/log/journal/ r,
+/run/log/journal/** r,
+/var/lib/systemd/catalog/database r,
+
+# Allow sysctl -w kernel.printk_ratelimit=#
+/{,usr/}sbin/sysctl ixr,
+@{PROC}/sys/kernel/printk_ratelimit rw,
+
+# Allow resolving kernel seccomp denials
+/usr/bin/scmp_sys_resolver ixr,
+
+# Needed since we are root and the owner/group doesn't match :\
+# So long as we have this, the cap must be reserved.
+capability dac_override,
+`
+
+// NewLogObserveInterface returns a new "log-observe" interface.
+func NewLogObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "log-observe",
+ connectedPlugAppArmor: logObserveConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type LogObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LogObserveInterfaceSuite{
+ iface: builtin.NewLogObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "log-observe",
+ Interface: "log-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "log-observe",
+ Interface: "log-observe",
+ },
+ },
+})
+
+func (s *LogObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "log-observe")
+}
+
+func (s *LogObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "log-observe",
+ Interface: "log-observe",
+ }})
+ c.Assert(err, ErrorMatches, "log-observe slots are reserved for the operating system snap")
+}
+
+func (s *LogObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *LogObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "log-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "log-observe"`)
+}
+
+func (s *LogObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const lxdConnectedPlugAppArmor = `
+# Description: allow access to the LXD daemon socket. This gives privileged
+# access to the system via LXD's socket API.
+
+/var/snap/lxd/common/lxd/unix.socket rw,
+`
+
+const lxdConnectedPlugSecComp = `
+# Description: allow access to the LXD daemon socket. This gives privileged
+# access to the system via LXD's socket API.
+
+shutdown
+`
+
+type LxdInterface struct{}
+
+func (iface *LxdInterface) Name() string {
+ return "lxd"
+}
+
+func (iface *LxdInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LxdInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(lxdConnectedPlugAppArmor), nil
+ case interfaces.SecuritySecComp:
+ return []byte(lxdConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *LxdInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LxdInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LxdInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *LxdInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *LxdInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const lxdSupportConnectedPlugAppArmor = `
+# Description: Can change to any apparmor profile (including unconfined) thus
+# giving access to all resources of the system so LXD may manage what to give
+# to its containers. This gives device ownership to connected snaps.
+@{PROC}/**/attr/current r,
+/usr/sbin/aa-exec ux,
+`
+
+const lxdSupportConnectedPlugSecComp = `
+# Description: Can access all syscalls of the system so LXD may manage what to
+# give to its containers, giving device ownership to connected snaps.
+@unrestricted
+`
+
+type LxdSupportInterface struct{}
+
+func (iface *LxdSupportInterface) Name() string {
+ return "lxd-support"
+}
+
+func (iface *LxdSupportInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LxdSupportInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(lxdSupportConnectedPlugAppArmor), nil
+ case interfaces.SecuritySecComp:
+ return []byte(lxdSupportConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *LxdSupportInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LxdSupportInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *LxdSupportInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *LxdSupportInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *LxdSupportInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type LxdSupportInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LxdSupportInterfaceSuite{
+ iface: &builtin.LxdSupportInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "lxd-support",
+ Interface: "lxd-support",
+ },
+ },
+
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "lxd",
+ },
+ Name: "lxd-support",
+ Interface: "lxd-support",
+ },
+ },
+})
+
+func (s *LxdSupportInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "lxd-support")
+}
+
+func (s *LxdSupportInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *LxdSupportInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *LxdSupportInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *LxdSupportInterfaceSuite) TestPermanentSlotPolicyAppArmor(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Check(string(snippet), testutil.Contains, "/usr/sbin/aa-exec ux,\n")
+}
+
+func (s *LxdSupportInterfaceSuite) TestPermanentSlotPolicySecComp(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Check(string(snippet), testutil.Contains, "@unrestricted\n")
+}
+
+func (s *LxdSupportInterfaceSuite) TestAutoConnect(c *C) {
+ c.Check(s.iface.AutoConnect(nil, nil), Equals, true)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type LxdInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&LxdInterfaceSuite{
+ iface: &builtin.LxdInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "lxd",
+ Interface: "lxd",
+ },
+ },
+
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "lxd",
+ },
+ Name: "lxd",
+ Interface: "lxd",
+ },
+ },
+})
+
+func (s *LxdInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "lxd")
+}
+
+func (s *LxdInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *LxdInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *LxdInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *LxdInterfaceSuite) TestConnectedPlugSnippetAppArmor(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Check(string(snippet), testutil.Contains, "/var/snap/lxd/common/lxd/unix.socket rw,\n")
+}
+
+func (s *LxdInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Check(string(snippet), testutil.Contains, "shutdown\n")
+}
+
+func (s *LxdInterfaceSuite) TestAutoConnect(c *C) {
+ // allow what declarations allowed
+ c.Check(s.iface.AutoConnect(nil, nil), Equals, true)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (c) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more dtails.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var mirPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the Mir server. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+# needed since Mir is the display server, to configure tty devices
+capability sys_tty_config,
+/{dev,run}/shm/\#* rw,
+/dev/tty[0-9]* rw,
+network netlink raw,
+/run/mir_socket rw,
+#NOTE: this allows reading and inserting all input events
+/dev/input/* rw,
+/run/udev/data/c13:[0-9]* r,
+/run/udev/data/+input:input[0-9]* r,
+`)
+
+var mirPermanentSlotSecComp = []byte(`
+# Description: Allow operating as the mir server. Reserved because this
+# gives privileged access to the system.
+# Needed for server launch
+bind
+listen
+setsockopt
+getsockname
+# Needed by server upon client connect
+sendto
+accept
+shmctl
+open
+getsockopt
+recvmsg
+sendmsg
+recvfrom
+`)
+
+var mirConnectedSlotAppArmor = []byte(`
+# Description: Permit clients to use Mir
+# Usage: reserved
+unix (receive, send) type=seqpacket addr=none peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var mirConnectedPlugAppArmor = []byte(`
+# Description: Permit clients to use Mir
+# Usage: common
+unix (receive, send) type=seqpacket addr=none peer=(label=###SLOT_SECURITY_TAGS###),
+/run/mir_socket rw,
+/run/user/[0-9]*/mir_socket rw,
+`)
+
+var mirConnectedPlugSecComp = []byte(`
+# Description: Permit clients to use Mir
+# Usage: common
+recvmsg
+sendmsg
+sendto
+recvfrom
+`)
+
+type MirInterface struct{}
+
+func (iface *MirInterface) Name() string {
+ return "mir"
+}
+
+func (iface *MirInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *MirInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(mirConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return mirConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *MirInterface) PermanentSlotSnippet(
+ slot *interfaces.Slot,
+ securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return mirPermanentSlotAppArmor, nil
+ case interfaces.SecuritySecComp:
+ return mirPermanentSlotSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *MirInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(mirConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *MirInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *MirInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *MirInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type MirInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&MirInterfaceSuite{
+ iface: &builtin.MirInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "mir-server", Type: snap.TypeOS},
+ Name: "mir-server",
+ Interface: "mir",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "mir-client",
+ Interface: "mir",
+ },
+ },
+})
+
+func (s *MirInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "mir")
+}
+
+func (s *MirInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp}
+ for _, system := range systems {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ if system != interfaces.SecuritySecComp {
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+)
+
+var modemManagerPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the ModemManager service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+# To check present devices
+/run/udev/data/* r,
+/sys/bus/usb/devices/ r,
+# FIXME snapd should be querying udev and adding the /sys and /run/udev accesses
+# that are assigned to the snap, but we are not there yet.
+/sys/bus/usb/devices/** r,
+
+# Access to modem ports
+# FIXME snapd should be more dynamic to avoid conflicts between snaps trying to
+# access same ports.
+/dev/tty[^0-9]* rw,
+/dev/cdc-* rw,
+
+# For ioctl TIOCSSERIAL ASYNC_CLOSING_WAIT_NONE
+capability sys_admin,
+
+include <abstractions/nameservice>
+
+# DBus accesses
+include <abstractions/dbus-strict>
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Request,Release}Name
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="org.freedesktop.ModemManager1",
+
+# Allow traffic to/from our path and interface with any method for unconfined
+# clients to talk to our modem-manager services.
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.ModemManager1*
+ peer=(label=unconfined),
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=unconfined),
+`)
+
+var modemManagerConnectedSlotAppArmor = []byte(`
+# Allow connected clients to interact with the service
+
+# Allow traffic to/from our path and interface with any method
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.ModemManager1*
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow traffic to/from org.freedesktop.DBus for ModemManager service
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var modemManagerConnectedPlugAppArmor = []byte(`
+# Description: Allow using ModemManager service. Reserved because this gives
+# privileged access to the ModemManager service.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Allow all access to ModemManager service
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.ModemManager1*
+ peer=(label=###SLOT_SECURITY_TAGS###),
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`)
+
+var modemManagerConnectedPlugAppArmorClassic = []byte(`
+# Allow access to the unconfined ModemManager service on classic.
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.ModemManager1*
+ peer=(label=unconfined),
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/ModemManager1{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=unconfined),
+`)
+
+var modemManagerPermanentSlotSecComp = []byte(`
+# Description: Allow operating as the ModemManager service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+# TODO: add ioctl argument filters when seccomp arg filtering is implemented
+accept
+accept4
+bind
+connect
+getpeername
+getsockname
+getsockopt
+listen
+recv
+recvfrom
+recvmmsg
+recvmsg
+send
+sendmmsg
+sendmsg
+sendto
+setsockopt
+shutdown
+socketpair
+socket
+`)
+
+var modemManagerConnectedPlugSecComp = []byte(`
+# Description: Allow using ModemManager service. Reserved because this gives
+# privileged access to the ModemManager service.
+# Usage: reserved
+
+# Can communicate with DBus system service
+connect
+getsockname
+recv
+recvmsg
+recvfrom
+send
+sendto
+sendmsg
+socket
+`)
+
+var modemManagerPermanentSlotDBus = []byte(`
+<policy user="root">
+ <allow own="org.freedesktop.ModemManager1"/>
+ <allow send_destination="org.freedesktop.ModemManager1"/>
+</policy>
+`)
+
+var modemManagerConnectedPlugDBus = []byte(`
+<policy context="default">
+ <deny own="org.freedesktop.ModemManager1"/>
+ <deny send_destination="org.freedesktop.ModemManager1"/>
+</policy>
+`)
+
+var modemManagerPermanentSlotUdev = []byte(`
+# Concatenation of all ModemManager udev rules
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_cinterion_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_cinterion_port_types_end"
+ENV{ID_VENDOR_ID}!="1e2d", GOTO="mm_cinterion_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0053", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_CINTERION_PORT_TYPE_GPS}="1"
+
+LABEL="mm_cinterion_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_mbm_end"
+SUBSYSTEMS=="usb", GOTO="mm_mbm_check"
+GOTO="mm_mbm_end"
+
+LABEL="mm_mbm_check"
+
+# Ericsson F3507g
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1900", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1902", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson F3607gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1904", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1905", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1906", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson F3307
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="190a", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1909", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson F3307 R2
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1914", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson C3607w
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1049", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson C3607w v2
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="190b", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson F5521gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="190d", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1911", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson H5321gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1919", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson H5321w
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="191d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson F5321gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1917", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson F5321w
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="191b", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson C5621gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="191f", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson C5621w
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1921", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson H5321gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1926", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1927", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson C3304w
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1928", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Ericsson C5621 TFF
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1936", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Sony-Ericsson MD300
+ATTRS{idVendor}=="0fce", ATTRS{idProduct}=="d0cf", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Sony-Ericsson MD400
+ATTRS{idVendor}=="0fce", ATTRS{idProduct}=="d0e1", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Sony-Ericsson MD400G
+ATTRS{idVendor}=="0fce", ATTRS{idProduct}=="d103", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Dell 5560
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="818e", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Dell 5550
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="818d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Dell 5530 HSDPA
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="8147", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Dell F3607gw
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="8183", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="8184", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Dell F3307
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="818b", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="413c", ATTRS{idProduct}=="818c", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# HP hs2330 Mobile Broadband Module
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="271d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# HP hs2320 Mobile Broadband Module
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="261d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# HP hs2340 Mobile Broadband Module
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="3a1d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# HP hs2350 Mobile Broadband Module
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="3d1d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# HP lc2000 Mobile Broadband Module
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="301d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# HP lc2010 Mobile Broadband Module
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="2f1d", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Toshiba
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="130b", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Toshiba F3607gw
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="130c", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1311", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Toshiba F3307
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1315", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1316", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1317", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Toshiba F5521gw
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1313", ENV{ID_MM_ERICSSON_MBM}="1"
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1314", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Toshiba H5321gw
+ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1319", ENV{ID_MM_ERICSSON_MBM}="1"
+
+# Lenovo N5321gw
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="193e", ENV{ID_MM_ERICSSON_MBM}="1"
+
+LABEL="mm_mbm_end"
+# do not edit this file, it will be overwritten on update
+ACTION!="add|change|move", GOTO="mm_huawei_port_types_end"
+
+ENV{ID_VENDOR_ID}!="12d1", GOTO="mm_huawei_port_types_end"
+
+# MU609 does not support getportmode (crashes modem with default firmware)
+ATTRS{idProduct}=="1573", ENV{ID_MM_HUAWEI_DISABLE_GETPORTMODE}="1"
+
+# Mark the modem and at port flags for ModemManager
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="ff", ATTRS{bInterfaceSubClass}=="01", ATTRS{bInterfaceProtocol}=="01", ENV{ID_MM_HUAWEI_MODEM_PORT}="1"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="ff", ATTRS{bInterfaceSubClass}=="01", ATTRS{bInterfaceProtocol}=="02", ENV{ID_MM_HUAWEI_AT_PORT}="1"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="ff", ATTRS{bInterfaceSubClass}=="02", ATTRS{bInterfaceProtocol}=="01", ENV{ID_MM_HUAWEI_MODEM_PORT}="1"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="ff", ATTRS{bInterfaceSubClass}=="02", ATTRS{bInterfaceProtocol}=="02", ENV{ID_MM_HUAWEI_AT_PORT}="1"
+
+# GPS NMEA port on MU609
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="ff", ATTRS{bInterfaceSubClass}=="01", ATTRS{bInterfaceProtocol}=="05", ENV{ID_MM_HUAWEI_GPS_PORT}="1"
+# GPS NMEA port on MU909
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="ff", ATTRS{bInterfaceSubClass}=="01", ATTRS{bInterfaceProtocol}=="14", ENV{ID_MM_HUAWEI_GPS_PORT}="1"
+
+# Only the standard ECM or NCM port can support dial-up with AT NDISDUP through AT port
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="02", ATTRS{bInterfaceSubClass}=="06",ATTRS{bInterfaceProtocol}=="00", ENV{ID_MM_HUAWEI_NDISDUP_SUPPORTED}="1"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="02", ATTRS{bInterfaceSubClass}=="0d",ATTRS{bInterfaceProtocol}=="00", ENV{ID_MM_HUAWEI_NDISDUP_SUPPORTED}="1"
+
+LABEL="mm_huawei_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+# Longcheer makes modules that other companies rebrand, like:
+#
+# Alcatel One Touch X020
+# Alcatel One Touch X030
+# MobiData MBD-200HU
+# ST Mobile Connect HSUPA USB Modem
+#
+# Most of these values were scraped from various Longcheer-based Windows
+# driver .inf files. cmmdm.inf lists the actual data (ie PPP) ports, while
+# cmser.inf lists the aux ports that may be either AT-capable or not but
+# cannot be used for PPP.
+
+
+ACTION!="add|change|move", GOTO="mm_longcheer_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_longcheer_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1c9e", GOTO="mm_longcheer_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1bbb", GOTO="mm_tamobile_vendorcheck"
+GOTO="mm_longcheer_port_types_end"
+
+LABEL="mm_longcheer_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="3197", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="3197", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="3197", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6000", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6000", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6000", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6060", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6060", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6060", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+# Alcatel One Touch X020
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6061", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6061", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="6061", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7001", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7001", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7001", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7001", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7002", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7002", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7002", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7002", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7002", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7101", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7101", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7101", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7101", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7102", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7102", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7102", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7102", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="7102", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8001", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8001", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8001", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8001", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8002", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8002", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8002", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8002", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+# ChinaBird PL68
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9000", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9000", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9000", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9001", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9001", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9001", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9001", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9002", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9002", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9002", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9002", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9003", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9003", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9003", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9003", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9003", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9004", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9004", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9004", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9005", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9005", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9005", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9010", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9010", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9010", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9010", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9012", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9012", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9012", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9012", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9020", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9020", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9020", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9020", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9022", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9022", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9022", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9022", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+# Zoom products
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9602", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9602", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9602", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9602", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9603", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9603", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9603", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9603", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9604", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9604", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9604", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9604", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9606", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9606", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9606", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9606", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9606", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9607", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9607", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9607", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9607", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9607", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9607", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+GOTO="mm_longcheer_port_types_end"
+
+
+LABEL="mm_tamobile_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# Alcatel One Touch X060s
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{ID_MM_LONGCHEER_TAGGED}="1"
+
+GOTO="mm_longcheer_port_types_end"
+
+
+LABEL="mm_longcheer_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_mtk_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="0e8d", GOTO="mm_mtk_port_types_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="2001", GOTO="mm_dlink_port_types_vendorcheck"
+GOTO="mm_mtk_port_types_end"
+
+# MediaTek devices ---------------------------
+
+LABEL="mm_mtk_port_types_vendorcheck"
+ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a1", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_MTK_MODEM_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a1", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_MTK_AT_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a1", ENV{ID_MM_MTK_TAGGED}="1"
+
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a2", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_MTK_MODEM_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a2", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_MTK_AT_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a2", ENV{ID_MM_MTK_TAGGED}="1"
+
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a4", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_MTK_MODEM_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a4", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_MTK_AT_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a4", ENV{ID_MM_MTK_TAGGED}="1"
+
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a5", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_MTK_MODEM_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a5", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_MTK_AT_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a5", ENV{ID_MM_MTK_TAGGED}="1"
+
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a7", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_MTK_MODEM_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a7", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_MTK_AT_PORT}="1"
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="00a7", ENV{ID_MM_MTK_TAGGED}="1"
+
+GOTO="mm_mtk_port_types_end"
+
+# D-Link devices ---------------------------
+
+LABEL="mm_dlink_port_types_vendorcheck"
+ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# D-Link DWM-156 A5 (and later?)
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7d00", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_MTK_MODEM_PORT}="1"
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7d00", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_MTK_AT_PORT}="1"
+ATTRS{idVendor}=="2001", ATTRS{idProduct}=="7d00", ENV{ID_MM_MTK_TAGGED}="1"
+
+GOTO="mm_mtk_port_types_end"
+
+LABEL="mm_mtk_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_nokia_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_nokia_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="0421", GOTO="mm_nokia_port_types_vendorcheck"
+GOTO="mm_nokia_port_types_end"
+
+LABEL="mm_nokia_port_types_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# For Nokia Internet Sticks (CS-xx) the modem/PPP port appears to always be USB interface 1
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="060D", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="0611", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="061A", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="061B", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="061F", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="0619", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="0620", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="0623", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="0624", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="0625", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="062A", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="062E", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="062F", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_NOKIA_PORT_TYPE_MODEM}="1"
+
+LABEL="mm_nokia_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_pcmcia_device_blacklist_end"
+SUBSYSTEM!="pcmcia", GOTO="mm_pcmcia_device_blacklist_end"
+
+# Gemplus Serial Port smartcard adapter
+ATTRS{prod_id1}=="Gemplus", ATTRS{prod_id2}=="SerialPort", ATTRS{prod_id3}=="GemPC Card", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+LABEL="mm_pcmcia_device_blacklist_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_platform_device_whitelist_end"
+SUBSYSTEM!="platform", GOTO="mm_platform_device_whitelist_end"
+
+# Be careful here since many devices connected to platform drivers on PCs
+# are legacy devices that won't like probing. But often on embedded
+# systems serial ports are provided by platform devices.
+
+# Allow atmel_usart
+DRIVERS=="atmel_usart", ENV{ID_MM_PLATFORM_DRIVER_PROBE}="1"
+
+LABEL="mm_platform_device_whitelist_end"
+# do not edit this file, it will be overwritten on update
+
+# Simtech makes modules that other companies rebrand, like:
+#
+# A-LINK 3GU
+# SCT UM300
+#
+# Most of these values were scraped from various SimTech-based Windows
+# driver .inf files. *mdm.inf lists the main command ports, while
+# *ser.inf lists the aux ports that may be used for PPP.
+
+
+ACTION!="add|change|move", GOTO="mm_simtech_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_simtech_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e0e", GOTO="mm_alink_vendorcheck"
+GOTO="mm_simtech_port_types_end"
+
+LABEL="mm_alink_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# A-LINK 3GU
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="cefe", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_SIMTECH_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="cefe", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_SIMTECH_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="cefe", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_SIMTECH_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="cefe", ENV{ID_MM_SIMTECH_TAGGED}="1"
+
+# Prolink PH-300
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="9100", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_SIMTECH_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="9100", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_SIMTECH_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="9100", ENV{ID_MM_SIMTECH_TAGGED}="1"
+
+# SCT UM300
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="9200", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_SIMTECH_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="9200", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_SIMTECH_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1e0e", ATTRS{idProduct}=="9200", ENV{ID_MM_SIMTECH_TAGGED}="1"
+
+GOTO="mm_simtech_port_types_end"
+
+LABEL="mm_simtech_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_telit_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_telit_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1bc7", GOTO="mm_telit_vendorcheck"
+GOTO="mm_telit_port_types_end"
+
+LABEL="mm_telit_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# UC864-E, UC864-E-AUTO, UC864-K, UC864-WD, UC864-WDU
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1003", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_TELIT_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1003", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_TELIT_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1003", ENV{ID_MM_TELIT_TAGGED}="1"
+
+# UC864-G
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1004", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_TELIT_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1004", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_TELIT_PORT_TYPE_NMEA}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1004", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_TELIT_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1004", ENV{ID_MM_TELIT_TAGGED}="1"
+
+# CC864-DUAL
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1005", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_TELIT_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1005", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_TELIT_PORT_TYPE_NMEA}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1005", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_TELIT_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1005", ENV{ID_MM_TELIT_TAGGED}="1"
+
+# CC864-SINGLE, CC864-KPS
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1006", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_TELIT_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1006", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_TELIT_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1006", ENV{ID_MM_TELIT_TAGGED}="1"
+
+# DE910-DUAL
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1010", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_TELIT_PORT_TYPE_NMEA}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1010", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_TELIT_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1010", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_TELIT_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1010", ENV{ID_MM_TELIT_TAGGED}="1"
+
+# CE910-DUAL
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1011", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_TELIT_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bc7", ATTRS{idProduct}=="1011", ENV{ID_MM_TELIT_TAGGED}="1"
+
+# NOTE: Qualcomm Gobi-based devices like the LE920 should not be handled
+# by this plugin, but by the Gobi plugin.
+
+GOTO="mm_telit_port_types_end"
+LABEL="mm_telit_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_usb_device_blacklist_end"
+SUBSYSTEM!="usb", GOTO="mm_usb_device_blacklist_end"
+ENV{DEVTYPE}!="usb_device", GOTO="mm_usb_device_blacklist_end"
+
+# APC UPS devices
+ATTRS{idVendor}=="051d", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Sweex 1000VA
+ATTRS{idVendor}=="0925", ATTRS{idProduct}=="1234", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Agiler UPS
+ATTRS{idVendor}=="05b8", ATTRS{idProduct}=="0000", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Krauler UP-M500VA
+ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0000", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Ablerex 625L USB
+ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="0000", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Belkin F6C1200-UNV
+ATTRS{idVendor}=="0665", ATTRS{idProduct}=="5161", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Various Liebert and Phoenixtec Power devices
+ATTRS{idVendor}=="06da", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Unitek Alpha 1200Sx
+ATTRS{idVendor}=="0f03", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Various Tripplite devices
+ATTRS{idVendor}=="09ae", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Various MGE Office Protection Systems devices
+ATTRS{idVendor}=="0463", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="0463", ATTRS{idProduct}=="ffff", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# CyberPower 900AVR/BC900D
+ATTRS{idVendor}=="0764", ATTRS{idProduct}=="0005", ENV{ID_MM_DEVICE_IGNORE}="1"
+# CyberPower CP1200AVR/BC1200D
+ATTRS{idVendor}=="0764", ATTRS{idProduct}=="0501", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Various Belkin devices
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0980", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0900", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0910", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0912", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0551", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0751", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0375", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="1100", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# HP R/T 2200 INTL (like SMART2200RMXL2U)
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="1f0a", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Powerware devices
+ATTRS{idVendor}=="0592", ATTRS{idProduct}=="0002", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Palm Treo 700/900/etc
+# Shouldn't be probed themselves, but you can install programs like
+# "MobileStream USB Modem" which changes the USB PID of the device to something
+# that isn't blacklisted.
+ATTRS{idVendor}=="0830", ATTRS{idProduct}=="0061", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# GlobalScaleTechnologies SheevaPlug
+ATTRS{idVendor}=="9e88", ATTRS{idProduct}=="9e8f", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Atmel Corp at91sam SAMBA bootloader
+ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="6124", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Dangerous Prototypes Bus Pirate v4
+ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="fb00", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# All devices from the Swiss Federal Institute of Technology
+ATTRS{idVendor}=="0617", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# West Mountain Radio devices
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="814a", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="814b", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="2405", ATTRS{idProduct}=="0003", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Arduinos
+ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9207", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1b4f", ATTRS{idProduct}=="9208", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Adafruit Flora
+ATTRS{idVendor}=="239a", ATTRS{idProduct}=="0004", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="239a", ATTRS{idProduct}=="8004", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# All devices from Pololu Corporation
+# except some possible future products.
+ATTRS{idVendor}=="1ffb", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1ffb", ATTRS{idProduct}=="00ad", ENV{ID_MM_DEVICE_IGNORE}="0"
+ATTRS{idVendor}=="1ffb", ATTRS{idProduct}=="00ae", ENV{ID_MM_DEVICE_IGNORE}="0"
+
+# Altair U-Boot device
+ATTRS{idVendor}=="0216", ATTRS{idProduct}=="0051", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Bluegiga BLE112B
+ATTRS{idVendor}=="2458", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Analog Devices BLIP camera
+ATTRS{idVendor}=="064b", ATTRS{idProduct}=="7823", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# MediaTek GPS chip (HOLUX M-1200E, GlobalTop Gms-d1, etc)
+ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="3329", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# PS-360 OEM (GPS sold with MS Street and Trips 2005)
+ATTRS{idVendor}=="067b", ATTRS{idProduct}=="aaa0", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# u-blox AG, u-blox 5 GPS chips
+ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a5", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a6", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Garmin GPS devices
+DRIVERS=="garmin_gps", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Cypress M8-based GPS devices, UPSes, and serial converters
+DRIVERS=="cypress_m8", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# All devices in the Openmoko vendor ID
+ATTRS{idVendor}=="1d50", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# All devices from 3D Robotics
+ATTRS{idVendor}=="26ac", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# empiriKit science lab controller device
+ATTRS{idVendor}=="0425", ATTRS{idProduct}=="0408", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+# Infineon Flashloader used by Intel XMM modem bootloader
+ATTRS{idVendor}=="8087", ATTRS{idProduct}=="0716", ENV{ID_MM_DEVICE_IGNORE}="1"
+
+LABEL="mm_usb_device_blacklist_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_usb_serial_adapters_greylist_end"
+SUBSYSTEM!="usb", GOTO="mm_usb_serial_adapters_greylist_end"
+ENV{DEVTYPE}!="usb_device", GOTO="mm_usb_serial_adapters_greylist_end"
+
+# Belkin F5U183 Serial Adapter
+ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0103", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# FTDI-based serial adapters
+# FTDI does USB to serial converter ICs; and it's very likely that they'll
+# never do modems themselves, so it should be safe to add a rule only based
+# on the vendor Id.
+ATTRS{idVendor}=="0403", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# ATEN Intl UC-232A (Prolific)
+ATTRS{idVendor}=="0557", ATTRS{idProduct}=="2008", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# Prolific USB to Serial adapter
+ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# Magic Control Technology Corp adapters
+ATTRS{idVendor}=="0711", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# Cygnal Integrated Products, Inc. CP210x
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea71", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# QinHeng Electronics HL-340
+ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# Atmel Corp. LUFA USB to Serial Adapter Project (Arduino)
+ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="204b", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+# Netchip Technology, Inc. Linux-USB Serial Gadget (CDC ACM mode)
+ATTRS{idVendor}=="0525", ATTRS{idProduct}=="a4a7", ENV{ID_MM_DEVICE_MANUAL_SCAN_ONLY}="1"
+
+LABEL="mm_usb_serial_adapters_greylist_end"
+# do not edit this file, it will be overwritten on update
+
+# Alcatel One Touch X220D
+# Alcatel One Touch X200
+#
+# These values were scraped from the X220D's Windows .inf files. jrdmdm.inf
+# lists the actual command and data (ie PPP) ports, while jrdser.inf lists the
+# aux ports that may be either AT-capable or not but cannot be used for PPP.
+
+
+ACTION!="add|change|move", GOTO="mm_x22x_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_x22x_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1bbb", GOTO="mm_x22x_generic_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="0b3c", GOTO="mm_x22x_olivetti_vendorcheck"
+GOTO="mm_x22x_port_types_end"
+
+# Generic JRD devices ---------------------------
+
+LABEL="mm_x22x_generic_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# Alcatel X200
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_X22X_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0000", ENV{ID_MM_X22X_TAGGED}="1"
+
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_X22X_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="0017", ENV{ID_MM_X22X_TAGGED}="1"
+
+# Archos G9
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="00B7", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_X22X_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="00B7", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="00B7", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_X22X_PORT_TYPE_NMEA}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="00B7", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_X22X_PORT_TYPE_VOICE}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="00B7", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="1bbb", ATTRS{idProduct}=="00B7", ENV{ID_MM_X22X_TAGGED}="1"
+
+GOTO="mm_x22x_port_types_end"
+
+# Olivetti devices ---------------------------
+
+LABEL="mm_x22x_olivetti_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# Olicard 200
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_X22X_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_X22X_PORT_TYPE_AUX}="1"
+ATTRS{idVendor}=="0b3c", ATTRS{idProduct}=="c005", ENV{ID_MM_X22X_TAGGED}="1"
+
+GOTO="mm_x22x_port_types_end"
+
+LABEL="mm_x22x_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move", GOTO="mm_zte_port_types_end"
+SUBSYSTEM!="tty", GOTO="mm_zte_port_types_end"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="19d2", GOTO="mm_zte_port_types_vendorcheck"
+GOTO="mm_zte_port_types_end"
+
+LABEL="mm_zte_port_types_vendorcheck"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0001", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0001", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0002", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0002", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0003", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0003", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0004", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0004", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0005", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0005", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0006", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0006", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0007", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0007", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0008", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0008", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0009", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0009", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="000A", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="000A", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0012", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0012", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0015", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0015", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0016", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0016", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0017", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0018", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0018", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0019", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0019", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0021", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0021", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0024", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0024", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0025", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0025", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0030", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0030", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0031", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0031", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0033", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0033", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0037", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0037", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0039", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0039", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0042", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0042", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0043", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0043", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0048", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0048", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0049", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0049", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0052", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0052", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0054", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0054", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0055", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0055", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0057", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0057", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0058", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0058", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0063", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0063", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0064", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0064", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0066", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0066", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0078", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0078", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0082", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0082", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0091", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0091", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0104", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0104", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0106", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0106", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0108", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0108", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0113", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0113", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0117", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0117", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0118", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0118", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0121", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0121", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0122", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0122", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0123", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0123", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0124", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0124", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0125", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0125", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0126", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0126", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0128", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="0128", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1007", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1007", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1008", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1008", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1010", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1010", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1254", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1254", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1515", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1515", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="2002", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="2002", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="2003", ENV{.MM_USBIFNUM}=="03", ENV{ID_MM_ZTE_PORT_TYPE_MODEM}="1"
+ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="2003", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_ZTE_PORT_TYPE_AUX}="1"
+
+# Icera-based devices that use DHCP, not AT%IPDPADDR
+ATTRS{product}=="K3805-z", ENV{ID_MM_ZTE_ICERA_DHCP}="1"
+
+LABEL="mm_zte_port_types_end"
+# do not edit this file, it will be overwritten on update
+
+# Tag any devices that MM might be interested in; if ModemManager is started
+# up right after udev, when MM explicitly requests devices on startup it may
+# get devices that haven't had all rules run yet. Thus, we tag devices we're
+# interested in and when handling devices during MM startup we ignore any
+# that don't have this tag. MM will still get the udev 'add' event for the
+# device a short while later and then process it as normal.
+
+ACTION!="add|change|move", GOTO="mm_candidate_end"
+
+SUBSYSTEM=="tty", ENV{ID_MM_CANDIDATE}="1"
+SUBSYSTEM=="net", ENV{ID_MM_CANDIDATE}="1"
+KERNEL=="cdc-wdm*", SUBSYSTEM=="usb", ENV{ID_MM_CANDIDATE}="1"
+KERNEL=="cdc-wdm*", SUBSYSTEM=="usbmisc", ENV{ID_MM_CANDIDATE}="1"
+
+LABEL="mm_candidate_end"
+`)
+
+type ModemManagerInterface struct{}
+
+func (iface *ModemManagerInterface) Name() string {
+ return "modem-manager"
+}
+
+func (iface *ModemManagerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *ModemManagerInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityDBus:
+ return []byte(modemManagerConnectedPlugDBus), nil
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace([]byte(modemManagerConnectedPlugAppArmor), old, new, -1)
+ if release.OnClassic {
+ // Let confined apps access unconfined ofono on classic
+ snippet = append(snippet, modemManagerConnectedPlugAppArmorClassic...)
+ }
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return modemManagerConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *ModemManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return modemManagerPermanentSlotAppArmor, nil
+ case interfaces.SecuritySecComp:
+ return modemManagerPermanentSlotSecComp, nil
+ case interfaces.SecurityUDev:
+ return modemManagerPermanentSlotUdev, nil
+ case interfaces.SecurityDBus:
+ return modemManagerPermanentSlotDBus, nil
+ }
+ return nil, nil
+}
+
+func (iface *ModemManagerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(modemManagerConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *ModemManagerInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *ModemManagerInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *ModemManagerInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type ModemManagerInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&ModemManagerInterfaceSuite{
+ iface: &builtin.ModemManagerInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "modem-manager"},
+ Name: "modem-manager",
+ Interface: "modem-manager",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "modem-manager"},
+ Name: "mmcli",
+ Interface: "modem-manager",
+ },
+ },
+})
+
+func (s *ModemManagerInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "modem-manager")
+}
+
+// The label glob when all apps are bound to the modem-manager slot
+func (s *ModemManagerInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "modem-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "modem-manager",
+ Interface: "modem-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.modem-manager.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the modem-manager slot
+func (s *ModemManagerInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "modem-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "modem-manager",
+ Interface: "modem-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.modem-manager.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the modem-manager slot
+func (s *ModemManagerInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "modem-manager",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "modem-manager",
+ Interface: "modem-manager",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.modem-manager.app"),`)
+}
+
+func (s *ModemManagerInterfaceSuite) TestConnectedPlugSnippetUsesUnconfinedLabelNot(c *C) {
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),")
+}
+
+func (s *ModemManagerInterfaceSuite) TestConnectedPlugSnippetUsesUnconfinedLabelOnClassic(c *C) {
+ release.OnClassic = true
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, "peer=(label=unconfined),")
+}
+
+func (s *ModemManagerInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *ModemManagerInterfaceSuite) TestPermanentSlotDBus(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, "allow own=\"org.freedesktop.ModemManager1\"")
+ c.Assert(string(snippet), testutil.Contains, "allow send_destination=\"org.freedesktop.ModemManager1\"")
+}
+
+func (s *ModemManagerInterfaceSuite) TestConnectedPlugDBus(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, "deny own=\"org.freedesktop.ModemManager1\"")
+ c.Assert(string(snippet), testutil.Contains, "deny send_destination=\"org.freedesktop.ModemManager1\"")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/mount-observe
+const mountObserveConnectedPlugAppArmor = `
+# Description: Can query system mount information. This is restricted because
+# it gives privileged read access to mount arguments and should only be used
+# with trusted apps.
+# Usage: reserved
+
+/{,usr/}bin/df ixr,
+
+# Needed by 'df'. This is an information leak
+@{PROC}/mounts r,
+owner @{PROC}/@{pid}/mounts r,
+owner @{PROC}/@{pid}/mountinfo r,
+owner @{PROC}/@{pid}/mountstats r,
+
+@{PROC}/swaps r,
+
+# This is often out of date but some apps insist on using it
+/etc/mtab r,
+/etc/fstab r,
+`
+
+// NewMountObserveInterface returns a new "mount-observe" interface.
+func NewMountObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "mount-observe",
+ connectedPlugAppArmor: mountObserveConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type MountObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&MountObserveInterfaceSuite{
+ iface: builtin.NewMountObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "mount-observe",
+ Interface: "mount-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "mount-observe",
+ Interface: "mount-observe",
+ },
+ },
+})
+
+func (s *MountObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "mount-observe")
+}
+
+func (s *MountObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "mount-observe",
+ Interface: "mount-observe",
+ }})
+ c.Assert(err, ErrorMatches, "mount-observe slots are reserved for the operating system snap")
+}
+
+func (s *MountObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *MountObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "mount-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "mount-observe"`)
+}
+
+func (s *MountObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+)
+
+var mprisPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as an MPRIS player.
+# Usage: common
+
+# DBus accesses
+#include <abstractions/dbus-session-strict>
+
+# https://specifications.freedesktop.org/mpris-spec/latest/
+# allow binding to the well-known DBus mpris interface based on the snap's name
+dbus (bind)
+ bus=session
+ name="org.mpris.MediaPlayer2.###MPRIS_NAME###{,.*}",
+
+# register as a player
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="{Request,Release}Name"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionUnix{ProcessID,User}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/org/mpris/MediaPlayer2
+ interface=org.freedesktop.DBus.Properties
+ member="{GetAll,PropertiesChanged}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/org/mpris/MediaPlayer2
+ interface="org.mpris.MediaPlayer2{,.Player}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# we can always connect to ourselves
+dbus (receive)
+ bus=session
+ path=/org/mpris/MediaPlayer2
+ peer=(label=@{profile_name}),
+`)
+
+var mprisConnectedSlotAppArmor = []byte(`
+# Allow connected clients to interact with the player
+dbus (receive)
+ bus=session
+ interface=org.freedesktop.DBus.Properties
+ path=/org/mpris/MediaPlayer2
+ peer=(label=###PLUG_SECURITY_TAGS###),
+dbus (receive)
+ bus=session
+ interface=org.freedesktop.DBus.Introspectable
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+dbus (receive)
+ bus=session
+ interface="org.mpris.MediaPlayer2{,.*}"
+ path=/org/mpris/MediaPlayer2
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var mprisConnectedSlotAppArmorClassic = []byte(`
+# Allow unconfined clients to interact with the player on classic
+dbus (receive)
+ bus=session
+ path=/org/mpris/MediaPlayer2
+ peer=(label=unconfined),
+dbus (receive)
+ bus=session
+ interface=org.freedesktop.DBus.Introspectable
+ peer=(label=unconfined),
+`)
+
+var mprisConnectedPlugAppArmor = []byte(`
+# Description: Allow connecting to an MPRIS player.
+# Usage: common
+
+#include <abstractions/dbus-session-strict>
+
+# Find the mpris player
+dbus (send)
+ bus=session
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus.Introspectable
+ peer=(name="org.freedesktop.DBus", label="unconfined"),
+dbus (send)
+ bus=session
+ path=/{,org,org/mpris,org/mpris/MediaPlayer2}
+ interface=org.freedesktop.DBus.Introspectable
+ peer=(name="org.freedesktop.DBus", label="unconfined"),
+# This reveals all names on the session bus
+dbus (send)
+ bus=session
+ path=/
+ interface=org.freedesktop.DBus
+ member=ListNames
+ peer=(name="org.freedesktop.DBus", label="unconfined"),
+
+# Communicate with the mpris player
+dbus (send)
+ bus=session
+ path=/org/mpris/MediaPlayer2
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`)
+
+var mprisPermanentSlotSecComp = []byte(`
+getsockname
+recvmsg
+sendmsg
+sendto
+`)
+
+var mprisConnectedPlugSecComp = []byte(`
+getsockname
+recvmsg
+sendmsg
+sendto
+`)
+
+type MprisInterface struct{}
+
+func (iface *MprisInterface) Name() string {
+ return "mpris"
+}
+
+func (iface *MprisInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *MprisInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(mprisConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return mprisConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *MprisInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ name, err := iface.getName(slot.Attrs)
+ if err != nil {
+ return nil, err
+ }
+
+ old := []byte("###MPRIS_NAME###")
+ new := []byte(name)
+ snippet := bytes.Replace(mprisPermanentSlotAppArmor, old, new, -1)
+ // on classic, allow unconfined remotes to control the player
+ // (eg, indicator-sound)
+ if release.OnClassic {
+ snippet = append(snippet, mprisConnectedSlotAppArmorClassic...)
+ }
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return mprisPermanentSlotSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *MprisInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(mprisConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *MprisInterface) getName(attribs map[string]interface{}) (string, error) {
+ // default to snap name if 'name' attribute not set
+ mprisName := "@{SNAP_NAME}"
+ for attr := range attribs {
+ if attr != "name" {
+ return "", fmt.Errorf("unknown attribute '%s'", attr)
+ }
+ raw, ok := attribs[attr]
+ if !ok {
+ return "", fmt.Errorf("cannot find attribute %q", attr)
+ }
+ name, ok := raw.(string)
+ if !ok {
+ return "", fmt.Errorf("name element %v is not a string", raw)
+ }
+
+ validDBusElement := regexp.MustCompile("^[a-zA-Z0-9_-]*$")
+ if !validDBusElement.MatchString(name) {
+ return "", fmt.Errorf("invalid name element: %q", name)
+ }
+ mprisName = name
+ }
+ return mprisName, nil
+}
+
+func (iface *MprisInterface) SanitizePlug(slot *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *MprisInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ _, err := iface.getName(slot.Attrs)
+ return err
+}
+
+func (iface *MprisInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type MprisInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&MprisInterfaceSuite{
+ iface: &builtin.MprisInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "mpris"},
+ Name: "mpris-player",
+ Interface: "mpris",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "mpris"},
+ Name: "mpris-client",
+ Interface: "mpris",
+ },
+ },
+})
+
+func (s *MprisInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "mpris")
+}
+
+func (s *MprisInterfaceSuite) TestGetName(c *C) {
+ const mockSnapYaml = `name: mpris-client
+version: 1.0
+slots:
+ mpris-slot:
+ interface: mpris
+ name: foo
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["mpris-slot"]}
+ iface := &builtin.MprisInterface{}
+ name, err := builtin.MprisGetName(iface, slot.Attrs)
+ c.Assert(err, IsNil)
+ c.Assert(name, Equals, "foo")
+}
+
+func (s *MprisInterfaceSuite) TestGetNameMissing(c *C) {
+ const mockSnapYaml = `name: mpris-client
+version: 1.0
+slots:
+ mpris-slot:
+ interface: mpris
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["mpris-slot"]}
+ iface := &builtin.MprisInterface{}
+ name, err := builtin.MprisGetName(iface, slot.Attrs)
+ c.Assert(err, IsNil)
+ c.Assert(name, Equals, "@{SNAP_NAME}")
+}
+func (s *MprisInterfaceSuite) TestGetNameBadDot(c *C) {
+ const mockSnapYaml = `name: mpris-client
+version: 1.0
+slots:
+ mpris-slot:
+ interface: mpris
+ name: foo.bar
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["mpris-slot"]}
+ iface := &builtin.MprisInterface{}
+ name, err := builtin.MprisGetName(iface, slot.Attrs)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "invalid name element: \"foo.bar\"")
+ c.Assert(name, Equals, "")
+}
+
+func (s *MprisInterfaceSuite) TestGetNameBadList(c *C) {
+ const mockSnapYaml = `name: mpris-client
+version: 1.0
+slots:
+ mpris-slot:
+ interface: mpris
+ name:
+ - foo
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["mpris-slot"]}
+ iface := &builtin.MprisInterface{}
+ name, err := builtin.MprisGetName(iface, slot.Attrs)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, `name element \[foo\] is not a string`)
+ c.Assert(name, Equals, "")
+}
+
+func (s *MprisInterfaceSuite) TestGetNameUnknownAttribute(c *C) {
+ const mockSnapYaml = `name: mpris-client
+version: 1.0
+slots:
+ mpris-slot:
+ interface: mpris
+ unknown: foo
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["mpris-slot"]}
+ iface := &builtin.MprisInterface{}
+ name, err := builtin.MprisGetName(iface, slot.Attrs)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, "unknown attribute 'unknown'")
+ c.Assert(name, Equals, "")
+}
+
+// The label glob when all apps are bound to the mpris slot
+func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "mpris",
+ Interface: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps are bound to the mpris slot
+func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "mpris",
+ Interface: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.{app1,app2}"),`)
+}
+
+func (s *MprisInterfaceSuite) TestConnectedPlugSecComp(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "getsockname\n")
+}
+
+// The label uses short form when exactly one app is bound to the mpris slot
+func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "mpris",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "mpris",
+ Interface: "mpris",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.app"),`)
+}
+
+// The label glob when all apps are bound to the mpris plug
+func (s *MprisInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "mpris",
+ Interface: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the mpris plug
+func (s *MprisInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "mpris",
+ Interface: "mpris",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the mpris plug
+func (s *MprisInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "mpris",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "mpris",
+ Interface: "mpris",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.app"),`)
+}
+
+func (s *MprisInterfaceSuite) TestPermanentSlotAppArmor(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify bind rule
+ c.Check(string(snippet), testutil.Contains, "dbus (bind)\n bus=session\n name=\"org.mpris.MediaPlayer2.@{SNAP_NAME}{,.*}\",\n")
+}
+
+func (s *MprisInterfaceSuite) TestPermanentSlotAppArmorWithName(c *C) {
+ const mockSnapYaml = `name: mpris-client
+version: 1.0
+slots:
+ mpris-slot:
+ interface: mpris
+ name: foo
+`
+ info := snaptest.MockInfo(c, mockSnapYaml, nil)
+ slot := &interfaces.Slot{SlotInfo: info.Slots["mpris-slot"]}
+ iface := &builtin.MprisInterface{}
+ snippet, err := iface.PermanentSlotSnippet(slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify bind rule
+ c.Check(string(snippet), testutil.Contains, "dbus (bind)\n bus=session\n name=\"org.mpris.MediaPlayer2.foo{,.*}\",\n")
+}
+
+func (s *MprisInterfaceSuite) TestPermanentSlotAppArmorNative(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+ iface := &builtin.MprisInterface{}
+ snippet, err := iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify classic rule not present
+ c.Check(string(snippet), Not(testutil.Contains), "# Allow unconfined clients to interact with the player on classic\n")
+}
+
+func (s *MprisInterfaceSuite) TestPermanentSlotAppArmorClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+ iface := &builtin.MprisInterface{}
+ snippet, err := iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // verify classic rule present
+ c.Check(string(snippet), testutil.Contains, "# Allow unconfined clients to interact with the player on classic\n")
+}
+
+func (s *MprisInterfaceSuite) TestPermanentSlotSecComp(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "getsockname\n")
+}
+
+func (s *MprisInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/network
+const networkConnectedPlugAppArmor = `
+# Description: Can access the network as a client.
+# Usage: common
+#include <abstractions/nameservice>
+#include <abstractions/ssl_certs>
+
+@{PROC}/sys/net/core/somaxconn r,
+@{PROC}/sys/net/ipv4/tcp_fastopen r,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/network
+const networkConnectedPlugSecComp = `
+# Description: Can access the network as a client.
+# Usage: common
+bind
+connect
+getpeername
+getsockname
+getsockopt
+recv
+recvfrom
+recvmmsg
+recvmsg
+send
+sendmmsg
+sendmsg
+sendto
+setsockopt
+shutdown
+
+# LP: #1446748 - limit this to AF_UNIX/AF_LOCAL and perhaps AF_NETLINK
+socket
+
+# This is an older interface and single entry point that can be used instead
+# of socket(), bind(), connect(), etc individually. While we could allow it,
+# we wouldn't be able to properly arg filter socketcall for AF_INET/AF_INET6
+# when LP: #1446748 is implemented.
+socketcall
+`
+
+// NewNetworkInterface returns a new "network" interface.
+func NewNetworkInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "network",
+ connectedPlugAppArmor: networkConnectedPlugAppArmor,
+ connectedPlugSecComp: networkConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/network-bind
+const networkBindConnectedPlugAppArmor = `
+# Description: Can access the network as a server.
+# Usage: common
+#include <abstractions/nameservice>
+#include <abstractions/ssl_certs>
+
+# These probably shouldn't be something that apps should use, but this offers
+# no information disclosure since the files are in the read-only part of the
+# system.
+/etc/hosts.deny r,
+/etc/hosts.allow r,
+
+@{PROC}/sys/net/core/somaxconn r,
+@{PROC}/sys/net/ipv4/ip_local_port_range r,
+
+# LP: #1496906: java apps need these for some reason and they leak the IPv6 IP
+# addresses and routes. Until we find another way to handle them (see the bug
+# for some options), we need to allow them to avoid developer confusion.
+@{PROC}/@{pid}/net/if_inet6 r,
+@{PROC}/@{pid}/net/ipv6_route r,
+
+# java apps request this but seem to work fine without it. Netlink sockets
+# are used to talk to kernel subsystems though and since apps run as root,
+# allowing blanket access needs to be carefully considered. Kernel capabilities
+# checks (which apparmor mediates) *should* be enough to keep abuse down,
+# however Linux capabilities can be quite broad and there have been CVEs in
+# this area. The issue is complicated because reservied policy groups like
+# 'network-admin' and 'network-firewall' have legitimate use for this rule,
+# however a network facing server shouldn't typically be running with these
+# policy groups. LP: #1499897
+# Note: for now, don't explicitly deny this noisy denial so --devmode isn't
+# broken but eventually we may conditionally deny this.
+#deny network netlink dgram,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/network-bind
+const networkBindConnectedPlugSecComp = `
+# Description: Can access the network as a server.
+# Usage: common
+accept
+accept4
+bind
+connect
+getpeername
+getsockname
+getsockopt
+listen
+recv
+recvfrom
+recvmmsg
+recvmsg
+send
+sendmmsg
+sendmsg
+sendto
+setsockopt
+shutdown
+
+# LP: #1446748 - limit this to AF_INET/AF_INET6
+socket
+
+# This is an older interface and single entry point that can be used instead
+# of socket(), bind(), connect(), etc individually. While we could allow it,
+# we wouldn't be able to properly arg filter socketcall for AF_INET/AF_INET6
+# when LP: #1446748 is implemented.
+socketcall
+`
+
+// NewNetworkBindInterface returns a new "network-bind" interface.
+func NewNetworkBindInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "network-bind",
+ connectedPlugAppArmor: networkBindConnectedPlugAppArmor,
+ connectedPlugSecComp: networkBindConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type NetworkBindInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&NetworkBindInterfaceSuite{
+ iface: builtin.NewNetworkBindInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "network-bind",
+ Interface: "network-bind",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "network-bind",
+ Interface: "network-bind",
+ },
+ },
+})
+
+func (s *NetworkBindInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "network-bind")
+}
+
+func (s *NetworkBindInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "network-bind",
+ Interface: "network-bind",
+ }})
+ c.Assert(err, ErrorMatches, "network-bind slots are reserved for the operating system snap")
+}
+
+func (s *NetworkBindInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *NetworkBindInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "network-bind"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "network-bind"`)
+}
+
+func (s *NetworkBindInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const networkControlConnectedPlugAppArmor = `
+# Description: Can configure networking and network namespaces via the standard
+# 'ip netns' command (man ip-netns(8)). This interface is restricted because it
+# gives wide, privileged access to networking and should only be used with
+# trusted apps.
+
+#include <abstractions/nameservice>
+#include <abstractions/ssl_certs>
+
+capability net_admin,
+capability net_raw,
+capability setuid, # ping
+
+# Allow protocols except those that we blacklist in
+# /etc/modprobe.d/blacklist-rare-network.conf
+network appletalk,
+network bridge,
+network inet,
+network inet6,
+network ipx,
+network packet,
+network pppox,
+network sna,
+
+@{PROC}/@{pid}/net/ r,
+@{PROC}/@{pid}/net/** r,
+
+# used by sysctl, et al
+@{PROC}/sys/ r,
+@{PROC}/sys/net/ r,
+@{PROC}/sys/net/core/ r,
+@{PROC}/sys/net/core/** rw,
+@{PROC}/sys/net/ipv{4,6}/ r,
+@{PROC}/sys/net/ipv{4,6}/** rw,
+@{PROC}/sys/net/netfilter/ r,
+@{PROC}/sys/net/netfilter/** rw,
+@{PROC}/sys/net/nf_conntrack_max rw,
+
+# networking tools
+/{,usr/}{,s}bin/arp ixr,
+/{,usr/}{,s}bin/arpd ixr,
+/{,usr/}{,s}bin/bridge ixr,
+/{,usr/}{,s}bin/dhclient Pxr, # use ixr instead if want to limit to snap dirs
+/{,usr/}{,s}bin/ifconfig ixr,
+/{,usr/}{,s}bin/ip ixr,
+/{,usr/}{,s}bin/ipmaddr ixr,
+/{,usr/}{,s}bin/iptunnel ixr,
+/{,usr/}{,s}bin/nameif ixr,
+/{,usr/}{,s}bin/netstat ixr, # -p not supported
+/{,usr/}{,s}bin/nstat ixr,
+/{,usr/}{,s}bin/ping ixr,
+/{,usr/}{,s}bin/ping6 ixr,
+/{,usr/}{,s}bin/pppd ixr,
+/{,usr/}{,s}bin/pppdump ixr,
+/{,usr/}{,s}bin/pppoe-discovery ixr,
+#/{,usr/}{,s}bin/pppstats ixr, # needs sys_module
+/{,usr/}{,s}bin/route ixr,
+/{,usr/}{,s}bin/routef ixr,
+/{,usr/}{,s}bin/routel ixr,
+/{,usr/}{,s}bin/rtacct ixr,
+/{,usr/}{,s}bin/rtmon ixr,
+/{,usr/}{,s}bin/sysctl ixr,
+/{,usr/}{,s}bin/tc ixr,
+/{,usr/}{,s}bin/wpa_action ixr,
+/{,usr/}{,s}bin/wpa_cli ixr,
+/{,usr/}{,s}bin/wpa_passphrase ixr,
+/{,usr/}{,s}bin/wpa_supplicant ixr,
+
+/dev/rfkill rw,
+
+# arp
+network netlink dgram,
+
+# ip, et al
+/etc/iproute2/ r,
+/etc/iproute2/* r,
+
+# ping - child profile would be nice but seccomp causes problems with that
+/{,usr/}{,s}bin/ping ixr,
+/{,usr/}{,s}bin/ping6 ixr,
+network inet raw,
+network inet6 raw,
+
+# pppd
+capability setuid,
+@{PROC}/@{pid}/loginuid r,
+@{PROC}/@{pid}/mounts r,
+
+# route
+/etc/networks r,
+
+# TUN/TAP
+/dev/net/tun rw,
+# These are dynamically created via ioctl() on /dev/net/tun
+/dev/tun[0-9]{,[0-9]*} rw,
+/dev/tap[0-9]{,[0-9]*} rw,
+
+# Network namespaces via 'ip netns'. In order to create network namespaces
+# that persist outside of the process and be entered (eg, via
+# 'ip netns exec ...') the ip command uses mount namespaces such that
+# applications can open the /run/netns/NAME object and use it with setns(2).
+# For 'ip netns exec' it will also create a mount namespace and bind mount
+# network configuration files into /etc in that namespace. See man ip-netns(8)
+# for details.
+
+capability sys_admin, # for setns()
+network netlink raw,
+
+/ r,
+/run/netns/ r, # only 'r' since snap-confine will create this for us
+/run/netns/* rw,
+mount options=(rw, rshared) -> /run/netns/,
+mount options=(rw, bind) /run/netns/ -> /run/netns/,
+mount options=(rw, bind) / -> /run/netns/*,
+umount /,
+
+# 'ip netns identify <pid>' and 'ip netns pids foo'
+capability sys_ptrace,
+# FIXME: ptrace can be used to break out of the seccomp sandbox unless the
+# kernel has 93e35efb8de45393cf61ed07f7b407629bf698ea (in 4.8+). Until this is
+# the default in snappy kernels, deny but audit as a reminder to get the
+# kernels patched.
+audit deny ptrace (trace) peer=snap.@{SNAP_NAME}.*, # eventually by default
+audit deny ptrace (trace), # for all other peers (process-control or other)
+
+# 'ip netns exec foo /bin/sh'
+mount options=(rw, rslave) /,
+mount options=(rw, rslave), # LP: #1648245
+umount /sys/,
+
+# Eg, nsenter --net=/run/netns/... <command>
+/{,usr/}{,s}bin/nsenter ixr,
+`
+
+const networkControlConnectedPlugSecComp = `
+# Description: Can configure networking and network namespaces via the standard
+# 'ip netns' command (man ip-netns(8)). This interface is restricted because it
+# gives wide, privileged access to networking and should only be used with
+# trusted apps.
+
+# for ping and ping6
+capset
+
+# Network namespaces via 'ip netns'. In order to create network namespaces
+# that persist outside of the process and be entered (eg, via
+# 'ip netns exec ...') the ip command uses mount namespaces such that
+# applications can open the /run/netns/NAME object and use it with setns(2).
+# For 'ip netns exec' it will also create a mount namespace and bind mount
+# network configuration files into /etc in that namespace. See man ip-netns(8)
+# for details.
+bind
+sendmsg
+sendto
+recvfrom
+recvmsg
+
+mount
+umount
+umount2
+
+unshare
+setns - CLONE_NEWNET
+`
+
+// NewNetworkControlInterface returns a new "network-control" interface.
+func NewNetworkControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "network-control",
+ connectedPlugAppArmor: networkControlConnectedPlugAppArmor,
+ connectedPlugSecComp: networkControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type NetworkControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&NetworkControlInterfaceSuite{
+ iface: builtin.NewNetworkControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "network-control",
+ Interface: "network-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "network-control",
+ Interface: "network-control",
+ },
+ },
+})
+
+func (s *NetworkControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "network-control")
+}
+
+func (s *NetworkControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "network-control",
+ Interface: "network-control",
+ }})
+ c.Assert(err, ErrorMatches, "network-control slots are reserved for the operating system snap")
+}
+
+func (s *NetworkControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *NetworkControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "network-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "network-control"`)
+}
+
+func (s *NetworkControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "/run/netns/* rw,\n")
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "setns - CLONE_NEWNET\n")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+)
+
+var networkManagerPermanentSlotAppArmor = []byte(`
+# Description: Allow operating as the NetworkManager service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+capability net_admin,
+capability net_bind_service,
+capability net_raw,
+
+network netlink,
+network netlink raw,
+network netlink dgram,
+network bridge,
+network inet,
+network inet6,
+network packet,
+
+@{PROC}/@{pid}/net/ r,
+@{PROC}/@{pid}/net/** r,
+
+# used by sysctl, et al
+@{PROC}/sys/ r,
+@{PROC}/sys/net/ r,
+@{PROC}/sys/net/core/ r,
+@{PROC}/sys/net/core/** rw,
+@{PROC}/sys/net/ipv{4,6}/ r,
+@{PROC}/sys/net/ipv{4,6}/** rw,
+@{PROC}/sys/net/netfilter/ r,
+@{PROC}/sys/net/netfilter/** rw,
+@{PROC}/sys/net/nf_conntrack_max rw,
+
+# Needed for systemd's dhcp implementation
+@{PROC}/sys/kernel/random/boot_id r,
+
+/sys/devices/**/**/net/**/phys_port_id r,
+/sys/devices/**/**/net/**/dev_id r,
+/sys/devices/virtual/net/**/phys_port_id r,
+/sys/devices/virtual/net/**/dev_id r,
+
+/dev/rfkill rw,
+
+/run/udev/data/* r,
+
+# Allow access to configuration files generated on the fly
+# from netplan and let NetworkManager store its DHCP leases
+# in the dhcp subdirectory so that console-conf can access
+# it.
+/run/NetworkManager/ w,
+/run/NetworkManager/{,**} r,
+/run/NetworkManager/dhcp/{,**} w,
+
+# Needed by the ifupdown plugin to check which interfaces can
+# be managed an which not.
+/etc/network/interfaces r,
+# Needed for systemd's dhcp implementation
+/etc/machine-id r,
+
+# Needed to use resolvconf from core
+/sbin/resolvconf ixr,
+/run/resolvconf/{,**} r,
+/run/resolvconf/** w,
+/etc/resolvconf/{,**} r,
+/lib/resolvconf/* ix,
+# Required by resolvconf
+/bin/run-parts ixr,
+/etc/resolvconf/update.d/* ix,
+
+#include <abstractions/nameservice>
+
+# DBus accesses
+#include <abstractions/dbus-strict>
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Request,Release}Name
+ peer=(name=org.freedesktop.DBus),
+
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member=GetConnectionUnixProcessID
+ peer=(label=unconfined),
+
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member=GetConnectionUnixUser
+ peer=(label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="org.freedesktop.NetworkManager",
+
+# Allow traffic to/from our path and interface with any method
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/NetworkManager{,/**}
+ interface=org.freedesktop.NetworkManager*,
+
+# Allow traffic to/from org.freedesktop.DBus for NetworkManager service
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/NetworkManager{,/**}
+ interface=org.freedesktop.DBus.*,
+
+# Allow access to hostname system service
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.DBus.Properties
+ peer=(label=unconfined),
+dbus(receive, send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.hostname1
+ member={Set,SetStatic}Hostname
+ peer=(label=unconfined),
+
+# Sleep monitor inside NetworkManager needs this
+dbus (send)
+ bus=system
+ path=/org/freedesktop/login1
+ member=Inhibit
+ interface=org.freedesktop.login1.Manager
+ peer=(label=unconfined),
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/login1
+ member=PrepareForSleep
+ interface=org.freedesktop.login1.Manager
+ peer=(label=unconfined),
+
+# Allow access to wpa-supplicant for managing WiFi networks
+dbus (receive, send)
+ bus=system
+ path=/fi/w1/wpa_supplicant1{,/**}
+ interface=fi.w1.wpa_supplicant1*
+ peer=(label=unconfined),
+dbus (receive, send)
+ bus=system
+ path=/fi/w1/wpa_supplicant1{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=unconfined),
+`)
+
+var networkManagerConnectedPlugAppArmor = []byte(`
+# Description: Allow using NetworkManager service. Reserved because this gives
+# privileged access to the NetworkManager service.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Allow all access to NetworkManager service
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/NetworkManager{,/**}
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`)
+
+var networkManagerPermanentSlotSecComp = []byte(`
+# Description: Allow operating as the NetworkManager service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+accept
+accept4
+bind
+connect
+getpeername
+getsockname
+getsockopt
+listen
+recv
+recvfrom
+recvmmsg
+recvmsg
+send
+sendmmsg
+sendmsg
+sendto
+setsockopt
+sethostname
+shutdown
+socketpair
+socket
+# Needed for keyfile settings plugin to allow adding settings
+# for different users. This is currently at runtime only used
+# to make new created network settings files only editable by
+# root:root. The existence of this chown call is only that its
+# used for some tests where a different user:group combination
+# will be supplied.
+# FIXME: adjust after seccomp argument filtering lands so that
+# we only allow chown and its variant to be called for root:root
+# and nothign else (LP: #1446748)
+chown
+chown32
+fchown
+fchown32
+fchownat
+lchown
+lchown32
+`)
+
+var networkManagerConnectedPlugSecComp = []byte(`
+# Description: Allow using NetworkManager service. Reserved because this gives
+# privileged access to the NetworkManager service.
+# Usage: reserved
+
+# Can communicate with DBus system service
+connect
+getsockname
+recv
+recvmsg
+recvfrom
+send
+sendto
+sendmsg
+socket
+`)
+
+var networkManagerPermanentSlotDBus = []byte(`
+<!-- DBus policy for NetworkManager (upstream version 1.2.2) -->
+<policy user="root">
+ <allow own="org.freedesktop.NetworkManager"/>
+ <allow send_destination="org.freedesktop.NetworkManager"/>
+
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.PPP"/>
+
+ <allow send_interface="org.freedesktop.NetworkManager.SecretAgent"/>
+
+ <!-- These are there because some broken policies do
+ <deny send_interface="..." /> (see dbus-daemon(8) for details).
+ This seems to override that for the known VPN plugins. -->
+ <allow send_destination="org.freedesktop.NetworkManager.openconnect"/>
+ <allow send_destination="org.freedesktop.NetworkManager.openswan"/>
+ <allow send_destination="org.freedesktop.NetworkManager.openvpn"/>
+ <allow send_destination="org.freedesktop.NetworkManager.pptp"/>
+ <allow send_destination="org.freedesktop.NetworkManager.vpnc"/>
+ <allow send_destination="org.freedesktop.NetworkManager.ssh"/>
+ <allow send_destination="org.freedesktop.NetworkManager.iodine"/>
+ <allow send_destination="org.freedesktop.NetworkManager.l2tp"/>
+ <allow send_destination="org.freedesktop.NetworkManager.libreswan"/>
+ <allow send_destination="org.freedesktop.NetworkManager.fortisslvpn"/>
+ <allow send_destination="org.freedesktop.NetworkManager.strongswan"/>
+ <allow send_interface="org.freedesktop.NetworkManager.VPN.Plugin"/>
+
+ <!-- Allow the custom name for the dnsmasq instance spawned by NM
+ from the dns dnsmasq plugin to own it's dbus name, and for
+ messages to be sent to it.
+ -->
+ <allow own="org.freedesktop.NetworkManager.dnsmasq"/>
+ <allow send_destination="org.freedesktop.NetworkManager.dnsmasq"/>
+</policy>
+
+<policy context="default">
+ <deny own="org.freedesktop.NetworkManager"/>
+
+ <deny send_destination="org.freedesktop.NetworkManager"/>
+
+ <!-- Basic D-Bus API stuff -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.DBus.Introspectable"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.DBus.Properties"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.DBus.ObjectManager"/>
+
+ <!-- Devices (read-only properties, no methods) -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Adsl"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Bond"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Bridge"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Bluetooth"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Wired"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Generic"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Gre"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Infiniband"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Macvlan"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Modem"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.OlpcMesh"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Team"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Tun"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Veth"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Vlan"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.WiMax.Nsp"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.AccessPoint"/>
+
+ <!-- Devices (read-only, no security required) -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.WiMax"/>
+
+ <!-- Devices (read/write, secured with PolicyKit) -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device.Wireless"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Device"/>
+
+ <!-- Core stuff (read-only properties, no methods) -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Connection.Active"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.DHCP4Config"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.DHCP6Config"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.IP4Config"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.IP6Config"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.VPN.Connection"/>
+
+ <!-- Core stuff (read/write, secured with PolicyKit) -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Settings"/>
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Settings.Connection"/>
+
+ <!-- Agents; secured with PolicyKit. Any process can talk to
+ the AgentManager API, but only NetworkManager can talk
+ to the agents themselves. -->
+ <allow send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.AgentManager"/>
+
+ <!-- Root-only functions -->
+ <deny send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager"
+ send_member="SetLogging"/>
+ <deny send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager"
+ send_member="Sleep"/>
+ <deny send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Settings"
+ send_member="LoadConnections"/>
+ <deny send_destination="org.freedesktop.NetworkManager"
+ send_interface="org.freedesktop.NetworkManager.Settings"
+ send_member="ReloadConnections"/>
+
+ <deny own="org.freedesktop.NetworkManager.dnsmasq"/>
+ <deny send_destination="org.freedesktop.NetworkManager.dnsmasq"/>
+</policy>
+
+<limit name="max_replies_per_connection">1024</limit>
+<limit name="max_match_rules_per_connection">2048</limit>
+`)
+
+type NetworkManagerInterface struct{}
+
+func (iface *NetworkManagerInterface) Name() string {
+ return "network-manager"
+}
+
+func (iface *NetworkManagerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *NetworkManagerInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityDBus:
+ return nil, nil
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ var new []byte
+ if release.OnClassic {
+ // If we're running on classic NetworkManager will be part
+ // of the OS snap and will run unconfined.
+ new = []byte("unconfined")
+ } else {
+ new = slotAppLabelExpr(slot)
+ }
+ snippet := bytes.Replace(networkManagerConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return networkManagerConnectedPlugSecComp, nil
+ }
+ return nil, nil
+}
+
+func (iface *NetworkManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return networkManagerPermanentSlotAppArmor, nil
+ case interfaces.SecuritySecComp:
+ return networkManagerPermanentSlotSecComp, nil
+ case interfaces.SecurityDBus:
+ return networkManagerPermanentSlotDBus, nil
+ }
+ return nil, nil
+}
+
+func (iface *NetworkManagerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *NetworkManagerInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *NetworkManagerInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *NetworkManagerInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type NetworkManagerInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&NetworkManagerInterfaceSuite{
+ iface: &builtin.NetworkManagerInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "network-manager"},
+ Name: "network-manager",
+ Interface: "network-manager",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "network-manager"},
+ Name: "nmcli",
+ Interface: "network-manager",
+ },
+ },
+})
+
+func (s *NetworkManagerInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "network-manager")
+}
+
+// The label glob when all apps are bound to the network-manager slot
+func (s *NetworkManagerInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "network-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "network-manager",
+ Interface: "network-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.network-manager.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the network-manager slot
+func (s *NetworkManagerInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "network-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "network-manager",
+ Interface: "network-manager",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.network-manager.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the network-manager slot
+func (s *NetworkManagerInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "network-manager",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "network-manager",
+ Interface: "network-manager",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.network-manager.app"),`)
+}
+
+func (s *NetworkManagerInterfaceSuite) TestConnectedPlugSnippedUsesUnconfinedLabelOnClassic(c *C) {
+ slot := &interfaces.Slot{}
+ release.OnClassic = true
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, "peer=(label=unconfined),")
+}
+
+func (s *NetworkManagerInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/network-observe
+const networkObserveConnectedPlugAppArmor = `
+# Description: Can query network status information. This is restricted because
+# it gives privileged read-only access to networking information and should
+# only be used with trusted apps.
+# Usage: reserved
+
+# network-monitor can't allow this otherwise we are basically
+# network-management, but don't explicitly deny since someone might try to use
+# network-management with network-monitor and that shouldn't fail weirdly
+#capability net_admin,
+
+#include <abstractions/nameservice>
+#include <abstractions/ssl_certs>
+
+@{PROC}/@{pid}/net/ r,
+@{PROC}/@{pid}/net/** r,
+
+# used by sysctl, et al (sysctl net)
+@{PROC}/sys/ r,
+@{PROC}/sys/net/ r,
+@{PROC}/sys/net/core/ r,
+@{PROC}/sys/net/core/** r,
+@{PROC}/sys/net/ipv{4,6}/ r,
+@{PROC}/sys/net/ipv{4,6}/** r,
+@{PROC}/sys/net/netfilter/ r,
+@{PROC}/sys/net/netfilter/** r,
+@{PROC}/sys/net/nf_conntrack_max r,
+
+# networking tools
+/{,usr/}{,s}bin/arp ixr,
+/{,usr/}{,s}bin/bridge ixr,
+/{,usr/}{,s}bin/ifconfig ixr,
+/{,usr/}{,s}bin/ip ixr,
+/{,usr/}{,s}bin/ipmaddr ixr,
+/{,usr/}{,s}bin/iptunnel ixr,
+/{,usr/}{,s}bin/netstat ixr, # -p not supported
+/{,usr/}{,s}bin/nstat ixr, # allows zeroing
+#/{,usr/}{,s}bin/pppstats ixr, # needs sys_module
+/{,usr/}{,s}bin/route ixr,
+/{,usr/}{,s}bin/routel ixr,
+/{,usr/}{,s}bin/rtacct ixr,
+/{,usr/}{,s}bin/sysctl ixr,
+/{,usr/}{,s}bin/tc ixr,
+
+# arp
+network netlink dgram,
+
+# ip, et al
+/etc/iproute2/ r,
+/etc/iproute2/* r,
+
+# ping - child profile would be nice but seccomp causes problems with that
+/{,usr/}{,s}bin/ping ixr,
+/{,usr/}{,s}bin/ping6 ixr,
+capability net_raw,
+capability setuid,
+network inet raw,
+network inet6 raw,
+
+# route
+/etc/networks r,
+
+# network devices
+/sys/devices/**/net/** r,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/network-observe
+const networkObserveConnectedPlugSecComp = `
+# Description: Can query network status information. This is restricted because
+# it gives privileged read-only access to networking information and should
+# only be used with trusted apps.
+# Usage: reserved
+
+# for ping and ping6
+capset
+`
+
+// NewNetworkObserveInterface returns a new "network-observe" interface.
+func NewNetworkObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "network-observe",
+ connectedPlugAppArmor: networkObserveConnectedPlugAppArmor,
+ connectedPlugSecComp: networkObserveConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type NetworkObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&NetworkObserveInterfaceSuite{
+ iface: builtin.NewNetworkObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "network-observe",
+ Interface: "network-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "network-observe",
+ Interface: "network-observe",
+ },
+ },
+})
+
+func (s *NetworkObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "network-observe")
+}
+
+func (s *NetworkObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "network-observe",
+ Interface: "network-observe",
+ }})
+ c.Assert(err, ErrorMatches, "network-observe slots are reserved for the operating system snap")
+}
+
+func (s *NetworkObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *NetworkObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "network-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "network-observe"`)
+}
+
+func (s *NetworkObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const networkSetupObserveConnectedPlugAppArmor = `
+# Description: Can read netplan configuration files
+# Usage: reserved
+
+/etc/netplan/{,**} r,
+/etc/network/{,**} r,
+`
+
+// NewNetworkSetupObserveInterface returns a new "network-setup-observe" interface.
+func NewNetworkSetupObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "network-setup-observe",
+ connectedPlugAppArmor: networkSetupObserveConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type NetworkSetupObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&NetworkSetupObserveInterfaceSuite{
+ iface: builtin.NewNetworkSetupObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "network-setup-observe",
+ Interface: "network-setup-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "network-setup-observe",
+ Interface: "network-setup-observe",
+ },
+ },
+})
+
+func (s *NetworkSetupObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "network-setup-observe")
+}
+
+func (s *NetworkSetupObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "network-setup-observe",
+ Interface: "network-setup-observe",
+ }})
+ c.Assert(err, ErrorMatches, "network-setup-observe slots are reserved for the operating system snap")
+}
+
+func (s *NetworkSetupObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *NetworkSetupObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "network-setup-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "network-setup-observe"`)
+}
+
+func (s *NetworkSetupObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type NetworkInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&NetworkInterfaceSuite{
+ iface: builtin.NewNetworkInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "network",
+ Interface: "network",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "network",
+ Interface: "network",
+ },
+ },
+})
+
+func (s *NetworkInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "network")
+}
+
+func (s *NetworkInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "network",
+ Interface: "network",
+ }})
+ c.Assert(err, ErrorMatches, "network slots are reserved for the operating system snap")
+}
+
+func (s *NetworkInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *NetworkInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "network"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "network"`)
+}
+
+func (s *NetworkInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+)
+
+const ofonoPermanentSlotAppArmor = `
+# Description: Allow operating as the ofono service. Reserved because this
+# gives privileged access to the system.
+
+# to create ppp network interfaces
+capability net_admin,
+
+# To check present devices
+/run/udev/data/+usb:* r,
+/run/udev/data/+usb-serial:* r,
+/run/udev/data/+pci:* r,
+/run/udev/data/+platform:* r,
+/run/udev/data/+pnp:* r,
+/run/udev/data/c* r,
+/run/udev/data/n* r,
+/sys/bus/usb/devices/ r,
+# FIXME snapd should be querying udev and adding the /sys and /run/udev accesses
+# that are assigned to the snap, but we are not there yet.
+/sys/bus/usb/devices/** r,
+
+# To get current seat, used to know user preferences like default SIM in
+# multi-SIM devices.
+/run/systemd/seats/{,*} r,
+
+# Access to modem ports
+# FIXME snapd should be more dynamic to avoid conflicts between snaps trying to
+# access same ports.
+/dev/tty[^0-9]* rw,
+/dev/cdc-* rw,
+/dev/modem* rw,
+/dev/dsp rw,
+/dev/chnlat11 rw,
+/dev/socket/rild* rw,
+# ofono puts ppp on top of the tun device
+/dev/net/tun rw,
+
+network netlink raw,
+network netlink dgram,
+network bridge,
+network inet,
+network inet6,
+network packet,
+network bluetooth,
+
+include <abstractions/nameservice>
+
+# DBus accesses
+include <abstractions/dbus-strict>
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Request,Release}Name
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="org.ofono",
+
+# Allow traffic to/from our path and interface with any method for unconfined
+# clients to talk to our ofono services.
+dbus (receive, send)
+ bus=system
+ path=/{,**}
+ interface=org.ofono.*
+ peer=(label=unconfined),
+`
+
+const ofonoConnectedSlotAppArmor = `
+# Allow service to interact with connected clients
+
+# Allow traffic to/from our interfaces. The path depends on the modem plugin,
+# and is arbitrary.
+dbus (receive, send)
+ bus=system
+ path=/{,**}
+ interface=org.ofono.*
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`
+
+const ofonoConnectedPlugAppArmor = `
+# Description: Allow using Ofono service. Reserved because this gives
+# privileged access to the Ofono service.
+
+#include <abstractions/dbus-strict>
+
+# Allow all access to ofono services
+dbus (receive, send)
+ bus=system
+ path=/{,**}
+ interface=org.ofono.*
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+const ofonoConnectedPlugAppArmorClassic = `
+# Allow access to the unconfined ofono services on classic.
+dbus (receive, send)
+ bus=system
+ path=/{,**}
+ interface=org.ofono.*
+ peer=(label=unconfined),
+`
+
+const ofonoPermanentSlotSecComp = `
+# Description: Allow operating as the ofono service. Reserved because this
+# gives privileged access to the system.
+
+# Communicate with DBus, netlink, rild
+accept
+accept4
+bind
+getsockopt
+listen
+recv
+recvfrom
+recvmmsg
+recvmsg
+send
+sendmmsg
+sendmsg
+sendto
+shutdown
+`
+
+const ofonoConnectedPlugSecComp = `
+# Description: Allow using ofono service. Reserved because this gives
+# privileged access to the ofono service.
+
+# Can communicate with DBus system service
+recv
+recvmsg
+recvfrom
+send
+sendto
+sendmsg
+`
+
+const ofonoPermanentSlotDBus = `
+<!-- Comes from src/ofono.conf in sources -->
+
+<policy user="root">
+ <allow own="org.ofono"/>
+ <allow send_destination="org.ofono"/>
+ <allow send_interface="org.ofono.SimToolkitAgent"/>
+ <allow send_interface="org.ofono.PushNotificationAgent"/>
+ <allow send_interface="org.ofono.SmartMessagingAgent"/>
+ <allow send_interface="org.ofono.PositioningRequestAgent"/>
+ <allow send_interface="org.ofono.HandsfreeAudioAgent"/>
+</policy>
+
+<policy context="default">
+ <deny send_destination="org.ofono"/>
+ <!-- Additional restriction in next line (not in ofono.conf) -->
+ <deny own="org.ofono"/>
+</policy>
+`
+
+const ofonoPermanentSlotUdev = `
+## Concatenation of all ofono udev rules (plugins/*.rules in ofono sources)
+## Note that ofono uses this for very few modems and that in most cases it finds
+## modems by checking directly in code udev events, so changes here will be rare
+
+## plugins/ofono.rules
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change", GOTO="ofono_end"
+
+# ISI/Phonet drivers
+SUBSYSTEM!="net", GOTO="ofono_isi_end"
+ATTRS{type}!="820", GOTO="ofono_isi_end"
+KERNELS=="gadget", GOTO="ofono_isi_end"
+
+# Nokia N900 modem
+SUBSYSTEMS=="hsi", ENV{OFONO_DRIVER}="n900", ENV{OFONO_ISI_ADDRESS}="108"
+KERNEL=="phonet*", ENV{OFONO_DRIVER}="n900", ENV{OFONO_ISI_ADDRESS}="108"
+
+# STE u8500
+KERNEL=="shrm0", ENV{OFONO_DRIVER}="u8500"
+
+LABEL="ofono_isi_end"
+
+SUBSYSTEM!="usb", GOTO="ofono_end"
+ENV{DEVTYPE}!="usb_device", GOTO="ofono_end"
+
+# Ignore fake serial number
+ATTRS{serial}=="1234567890ABCDEF", ENV{ID_SERIAL_SHORT}=""
+
+# Nokia CDMA Device
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="023e", ENV{OFONO_DRIVER}="nokiacdma"
+ATTRS{idVendor}=="0421", ATTRS{idProduct}=="00b6", ENV{OFONO_DRIVER}="nokiacdma"
+
+# Lenovo H5321gw 0bdb:1926
+ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1926", ENV{OFONO_DRIVER}="mbm"
+
+LABEL="ofono_end"
+
+## plugins/ofono-speedup.rules
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change", GOTO="ofono_speedup_end"
+
+SUBSYSTEM!="tty", GOTO="ofono_speedup_end"
+KERNEL!="ttyUSB[0-9]*", GOTO="ofono_speedup_end"
+
+# SpeedUp 7300
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9e00", ENV{ID_USB_INTERFACE_NUM}=="00", ENV{OFONO_LABEL}="modem"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9e00", ENV{ID_USB_INTERFACE_NUM}=="03", ENV{OFONO_LABEL}="aux"
+
+# SpeedUp
+ATTRS{idVendor}=="2020", ATTRS{idProduct}=="1005", ENV{ID_USB_INTERFACE_NUM}=="03", ENV{OFONO_LABEL}="modem"
+ATTRS{idVendor}=="2020", ATTRS{idProduct}=="1005", ENV{ID_USB_INTERFACE_NUM}=="01", ENV{OFONO_LABEL}="aux"
+
+ATTRS{idVendor}=="2020", ATTRS{idProduct}=="1008", ENV{ID_USB_INTERFACE_NUM}=="03", ENV{OFONO_LABEL}="modem"
+ATTRS{idVendor}=="2020", ATTRS{idProduct}=="1008", ENV{ID_USB_INTERFACE_NUM}=="01", ENV{OFONO_LABEL}="aux"
+
+# SpeedUp 9800
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9800", ENV{ID_USB_INTERFACE_NUM}=="01", ENV{OFONO_LABEL}="modem"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9800", ENV{ID_USB_INTERFACE_NUM}=="02", ENV{OFONO_LABEL}="aux"
+
+# SpeedUp U3501
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{ID_USB_INTERFACE_NUM}=="03", ENV{OFONO_LABEL}="modem"
+ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="9605", ENV{ID_USB_INTERFACE_NUM}=="01", ENV{OFONO_LABEL}="aux"
+
+LABEL="ofono_speedup_end"
+`
+
+type OfonoInterface struct{}
+
+func (iface *OfonoInterface) Name() string {
+ return "ofono"
+}
+
+func (iface *OfonoInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *OfonoInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace([]byte(ofonoConnectedPlugAppArmor), old, new, -1)
+ if release.OnClassic {
+ // Let confined apps access unconfined ofono on classic
+ snippet = append(snippet, ofonoConnectedPlugAppArmorClassic...)
+ }
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return []byte(ofonoConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *OfonoInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(ofonoPermanentSlotAppArmor), nil
+ case interfaces.SecuritySecComp:
+ return []byte(ofonoPermanentSlotSecComp), nil
+ case interfaces.SecurityUDev:
+ return []byte(ofonoPermanentSlotUdev), nil
+ case interfaces.SecurityDBus:
+ return []byte(ofonoPermanentSlotDBus), nil
+ }
+ return nil, nil
+}
+
+func (iface *OfonoInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace([]byte(ofonoConnectedSlotAppArmor), old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *OfonoInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *OfonoInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *OfonoInterface) LegacyAutoConnect() bool {
+ return false
+}
+
+func (iface *OfonoInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type OfonoInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&OfonoInterfaceSuite{
+ iface: &builtin.OfonoInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "ofono"},
+ Name: "ofono",
+ Interface: "ofono",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "ofono"},
+ Name: "dbus-send",
+ Interface: "ofono",
+ },
+ },
+})
+
+func (s *OfonoInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "ofono")
+}
+
+// The label glob when all apps are bound to the ofono slot
+func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "ofono",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "ofono",
+ Interface: "ofono",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.ofono.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the ofono slot
+func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "ofono",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "ofono",
+ Interface: "ofono",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.ofono.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the ofono slot
+func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "ofono",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "ofono",
+ Interface: "ofono",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.ofono.app"),`)
+}
+
+func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetUsesUnconfinedLabelOnClassic(c *C) {
+ release.OnClassic = true
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ // verify apparmor connected
+ c.Assert(string(snippet), testutil.Contains, "#include <abstractions/dbus-strict>")
+ // verify classic connected
+ c.Assert(string(snippet), testutil.Contains, "peer=(label=unconfined),")
+}
+
+func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetAppArmor(c *C) {
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ // verify apparmor connected
+ c.Assert(string(snippet), testutil.Contains, "#include <abstractions/dbus-strict>")
+ // verify classic didn't connect
+ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),")
+}
+
+func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "send\n")
+}
+
+func (s *OfonoInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) {
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "peer=(label=\"snap.ofono.*\")")
+}
+
+func (s *OfonoInterfaceSuite) TestPermanentSlotSnippetAppArmor(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "/dev/net/tun rw,")
+}
+
+func (s *OfonoInterfaceSuite) TestPermanentSlotSnippetSecComp(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ c.Check(string(snippet), testutil.Contains, "listen\n")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const openglConnectedPlugAppArmor = `
+# Description: Can access opengl.
+# Usage: reserved
+
+ # specific gl libs
+ /var/lib/snapd/lib/gl/ r,
+ /var/lib/snapd/lib/gl/** rm,
+
+ /dev/dri/card0 rw,
+ # nvidia
+ @{PROC}/driver/nvidia/params r,
+ @{PROC}/modules r,
+ /dev/nvidiactl rw,
+ /dev/nvidia-modeset rw,
+ /dev/nvidia* rw,
+
+ # eglfs
+ /dev/vchiq rw,
+
+ # FIXME: this is an information leak and snapd should instead query udev for
+ # the specific accesses associated with the above devices.
+ /sys/bus/pci/devices/** r,
+ /run/udev/data/+drm:card* r,
+ /run/udev/data/+pci:[0-9]* r,
+
+ # FIXME: for each device in /dev that this policy references, lookup the
+ # device type, major and minor and create rules of this form:
+ # /run/udev/data/<type><major>:<minor> r,
+ # For now, allow 'c'haracter devices and 'b'lock devices based on
+ # https://www.kernel.org/doc/Documentation/devices.txt
+ /run/udev/data/c226:[0-9]* r, # 226 drm
+`
+
+const openglConnectedPlugSecComp = `
+# Description: Can access opengl.
+# Usage: reserved
+
+getsockopt
+`
+
+// NewOpenglInterface returns a new "opengl" interface.
+func NewOpenglInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "opengl",
+ connectedPlugAppArmor: openglConnectedPlugAppArmor,
+ connectedPlugSecComp: openglConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const openvswitchConnectedPlugAppArmor = `
+/run/openvswitch/db.sock rw,
+`
+
+const openvswitchConnectedPlugSecComp = `
+connect
+recv
+recvmsg
+send
+sendto
+sendmsg
+socket
+socketpair
+`
+
+func NewOpenvSwitchInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "openvswitch",
+ connectedPlugAppArmor: openvswitchConnectedPlugAppArmor,
+ connectedPlugSecComp: openvswitchConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const openvswitchSupportConnectedPlugKmod = `
+openvswitch
+`
+
+// NewOpenvSwitchSupportInterface returns a new "openvswitch-support" interface.
+func NewOpenvSwitchSupportInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "openvswitch-support",
+ connectedPlugKMod: openvswitchSupportConnectedPlugKmod,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type OpenvSwitchSupportInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&OpenvSwitchSupportInterfaceSuite{
+ iface: builtin.NewOpenvSwitchSupportInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "openvswitch-support",
+ Interface: "openvswitch-support",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "openvswitch-support",
+ Interface: "openvswitch-support",
+ },
+ },
+})
+
+func (s *OpenvSwitchSupportInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "openvswitch-support")
+}
+
+func (s *OpenvSwitchSupportInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "openvswitch-support",
+ Interface: "openvswitch-support",
+ }})
+ c.Assert(err, ErrorMatches, "openvswitch-support slots are reserved for the operating system snap")
+}
+
+func (s *OpenvSwitchSupportInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *OpenvSwitchSupportInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "openvswitch-support"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "openvswitch-support"`)
+}
+
+func (s *OpenvSwitchSupportInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for kmod
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityKMod)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type OpenvSwitchInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&OpenvSwitchInterfaceSuite{
+ iface: builtin.NewOpenvSwitchInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "openvswitch",
+ Interface: "openvswitch",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "openvswitch",
+ Interface: "openvswitch",
+ },
+ },
+})
+
+func (s *OpenvSwitchInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "openvswitch")
+}
+
+func (s *OpenvSwitchInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "openvswitch",
+ Interface: "openvswitch",
+ }})
+ c.Assert(err, ErrorMatches, "openvswitch slots are reserved for the operating system snap")
+}
+
+func (s *OpenvSwitchInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *OpenvSwitchInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "openvswitch"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "openvswitch"`)
+}
+
+func (s *OpenvSwitchInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const opticalDriveConnectedPlugAppArmor = `
+/dev/sr[0-9]* r,
+/dev/scd[0-9]* r,
+`
+
+// NewOpticalDriveInterface returns a new "optical-drive" interface.
+func NewOpticalDriveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "optical-drive",
+ connectedPlugAppArmor: opticalDriveConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const physicalMemoryControlConnectedPlugAppArmor = `
+# Description: With kernels with STRICT_DEVMEM=n, write access to all physical
+# memory.
+#
+# With STRICT_DEVMEM=y, allow writing to /dev/mem to access
+# architecture-specific subset of the physical address (eg, PCI space,
+# BIOS code and data regions on x86, etc) for all common uses of /dev/mem
+# (eg, X without KMS, dosemu, etc).
+capability sys_rawio,
+/dev/mem rw,
+`
+
+// The type for physical-memory-control interface
+type PhysicalMemoryControlInterface struct{}
+
+// Getter for the name of the physical-memory-control interface
+func (iface *PhysicalMemoryControlInterface) Name() string {
+ return "physical-memory-control"
+}
+
+func (iface *PhysicalMemoryControlInterface) String() string {
+ return iface.Name()
+}
+
+// Check validity of the defined slot
+func (iface *PhysicalMemoryControlInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Does it have right type?
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // Creation of the slot of this type
+ // is allowed only by a gadget or os snap
+ if !(slot.Snap.Type == "os") {
+ return fmt.Errorf("%s slots only allowed on core snap", iface.Name())
+ }
+ return nil
+}
+
+// Checks and possibly modifies a plug
+func (iface *PhysicalMemoryControlInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // Currently nothing is checked on the plug side
+ return nil
+}
+
+// Returns snippet granted on install
+func (iface *PhysicalMemoryControlInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// Getter for the security snippet specific to the plug
+func (iface *PhysicalMemoryControlInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(physicalMemoryControlConnectedPlugAppArmor), nil
+
+ case interfaces.SecurityUDev:
+ var tagSnippet bytes.Buffer
+ const udevRule = `KERNEL=="mem", TAG+="%s"`
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ tagSnippet.WriteString(fmt.Sprintf(udevRule, tag))
+ tagSnippet.WriteString("\n")
+ }
+ return tagSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+// No extra permissions granted on connection
+func (iface *PhysicalMemoryControlInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// No permissions granted to plug permanently
+func (iface *PhysicalMemoryControlInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PhysicalMemoryControlInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // Allow what is allowed in the declarations
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type PhysicalMemoryControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&PhysicalMemoryControlInterfaceSuite{
+ iface: &builtin.PhysicalMemoryControlInterface{},
+})
+
+func (s *PhysicalMemoryControlInterfaceSuite) SetUpTest(c *C) {
+ // Mock for OS Snap
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-physical-memory:
+ interface: physical-memory-control
+`, nil)
+ s.slot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-physical-memory"]}
+
+ // Snap Consumers
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-physical-memory:
+ interface: physical-memory-control
+apps:
+ app-accessing-physical-memory:
+ command: foo
+ plugs: [plug-for-physical-memory]
+`, nil)
+ s.plug = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-physical-memory"]}
+}
+
+func (s *PhysicalMemoryControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "physical-memory-control")
+}
+
+func (s *PhysicalMemoryControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "physical-memory-control",
+ Interface: "physical-memory-control",
+ }})
+ c.Assert(err, ErrorMatches, "physical-memory-control slots only allowed on core snap")
+}
+
+func (s *PhysicalMemoryControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *PhysicalMemoryControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "physical-memory-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "physical-memory-control"`)
+}
+
+func (s *PhysicalMemoryControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ expectedSnippet1 := []byte(`
+# Description: With kernels with STRICT_DEVMEM=n, write access to all physical
+# memory.
+#
+# With STRICT_DEVMEM=y, allow writing to /dev/mem to access
+# architecture-specific subset of the physical address (eg, PCI space,
+# BIOS code and data regions on x86, etc) for all common uses of /dev/mem
+# (eg, X without KMS, dosemu, etc).
+capability sys_rawio,
+/dev/mem rw,
+`)
+ expectedSnippet2 := []byte(`KERNEL=="mem", TAG+="snap_client-snap_app-accessing-physical-memory"
+`)
+
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const physicalMemoryObserveConnectedPlugAppArmor = `
+# Description: With kernels with STRICT_DEVMEM=n, read-only access to all physical
+# memory. With STRICT_DEVMEM=y, allow reading /dev/mem for read-only
+# access to architecture-specific subset of the physical address (eg, PCI,
+# space, BIOS code and data regions on x86, etc).
+/dev/mem r,
+`
+
+// The type for physical-memory-observe interface
+type PhysicalMemoryObserveInterface struct{}
+
+// Getter for the name of the physical-memory-observe interface
+func (iface *PhysicalMemoryObserveInterface) Name() string {
+ return "physical-memory-observe"
+}
+
+func (iface *PhysicalMemoryObserveInterface) String() string {
+ return iface.Name()
+}
+
+// Check validity of the defined slot
+func (iface *PhysicalMemoryObserveInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Does it have right type?
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // Creation of the slot of this type
+ // is allowed only by a gadget or os snap
+ if !(slot.Snap.Type == "os") {
+ return fmt.Errorf("%s slots only allowed on core snap", iface.Name())
+ }
+ return nil
+}
+
+// Checks and possibly modifies a plug
+func (iface *PhysicalMemoryObserveInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // Currently nothing is checked on the plug side
+ return nil
+}
+
+// Returns snippet granted on install
+func (iface *PhysicalMemoryObserveInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// Getter for the security snippet specific to the plug
+func (iface *PhysicalMemoryObserveInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(physicalMemoryObserveConnectedPlugAppArmor), nil
+
+ case interfaces.SecurityUDev:
+ var tagSnippet bytes.Buffer
+ const udevRule = `KERNEL=="mem", TAG+="%s"`
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ tagSnippet.WriteString(fmt.Sprintf(udevRule, tag))
+ tagSnippet.WriteString("\n")
+ }
+ return tagSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+// No extra permissions granted on connection
+func (iface *PhysicalMemoryObserveInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// No permissions granted to plug permanently
+func (iface *PhysicalMemoryObserveInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PhysicalMemoryObserveInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // Allow what is allowed in the declarations
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type PhysicalMemoryObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&PhysicalMemoryObserveInterfaceSuite{
+ iface: &builtin.PhysicalMemoryObserveInterface{},
+})
+
+func (s *PhysicalMemoryObserveInterfaceSuite) SetUpTest(c *C) {
+ // Mock for OS Snap
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-physical-memory:
+ interface: physical-memory-observe
+`, nil)
+ s.slot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-physical-memory"]}
+
+ // Snap Consumers
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-physical-memory:
+ interface: physical-memory-observe
+apps:
+ app-accessing-physical-memory:
+ command: foo
+ plugs: [plug-for-physical-memory]
+`, nil)
+ s.plug = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-physical-memory"]}
+}
+
+func (s *PhysicalMemoryObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "physical-memory-observe")
+}
+
+func (s *PhysicalMemoryObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "physical-memory-observe",
+ Interface: "physical-memory-observe",
+ }})
+ c.Assert(err, ErrorMatches, "physical-memory-observe slots only allowed on core snap")
+}
+
+func (s *PhysicalMemoryObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *PhysicalMemoryObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "physical-memory-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "physical-memory-observe"`)
+}
+
+func (s *PhysicalMemoryObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ expectedSnippet1 := []byte(`
+# Description: With kernels with STRICT_DEVMEM=n, read-only access to all physical
+# memory. With STRICT_DEVMEM=y, allow reading /dev/mem for read-only
+# access to architecture-specific subset of the physical address (eg, PCI,
+# space, BIOS code and data regions on x86, etc).
+/dev/mem r,
+`)
+ expectedSnippet2 := []byte(`KERNEL=="mem", TAG+="snap_client-snap_app-accessing-physical-memory"
+`)
+
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+var pppConnectedPlugAppArmor = []byte(`
+# Description: Allow operating ppp daemon. Reserved because this gives
+# privileged access to the ppp daemon.
+# Usage: reserved
+
+# Needed for modem connections using PPP
+/usr/sbin/pppd ix,
+/etc/ppp/** rwix,
+/dev/ppp rw,
+/dev/tty[^0-9]* rw,
+/run/lock/*tty[^0-9]* rw,
+/run/ppp* rw,
+/var/run/ppp* rw,
+/var/log/ppp* rw,
+/bin/run-parts ix,
+@{PROC}/@{pid}/loginuid r,
+capability setgid,
+capability setuid,
+`)
+
+// ppp_generic creates /dev/ppp. Other ppp modules will be automatically loaded
+// by the kernel on different ioctl calls for this device. Note also that
+// in many cases ppp_generic is statically linked into the kernel (CONFIG_PPP=y)
+var pppConnectedPlugKmod = []byte(`
+ppp_generic
+`)
+
+type PppInterface struct{}
+
+func (iface *PppInterface) Name() string {
+ return "ppp"
+}
+
+func (iface *PppInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PppInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return pppConnectedPlugAppArmor, nil
+ case interfaces.SecurityKMod:
+ return pppConnectedPlugKmod, nil
+ }
+ return nil, nil
+}
+
+func (iface *PppInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PppInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PppInterface) SanitizePlug(plug *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *PppInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *PppInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type PppInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&PppInterfaceSuite{
+ iface: &builtin.PppInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "ppp"},
+ Name: "ppp",
+ Interface: "ppp",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "ppp"},
+ Name: "mmcli",
+ Interface: "ppp",
+ },
+ },
+})
+
+func (s *PppInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "ppp")
+}
+
+func (s *PppInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp, interfaces.SecurityKMod}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ if system == interfaces.SecurityAppArmor || system == interfaces.SecurityKMod {
+ c.Assert(snippet, Not(IsNil))
+ } else {
+ c.Assert(snippet, IsNil)
+ }
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const processControlConnectedPlugAppArmor = `
+# Description: This interface allows for controlling other processes via
+# signals and nice. This is reserved because it grants privileged access to
+# all processes under root or processes running under the same UID otherwise.
+# Usage: reserved
+
+/{,usr/}bin/nice ixr,
+
+capability sys_resource,
+capability sys_nice,
+
+signal,
+`
+
+const processControlConnectedPlugSecComp = `
+# Description: This interface allows for controlling other processes via
+# signals and nice. This is reserved because it grants privileged access to
+# all processes under root or processes running under the same UID otherwise.
+# Usage: reserved
+
+setpriority
+sched_setaffinity
+`
+
+// NewProcessControlInterface returns a new "process-control" interface.
+func NewProcessControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "process-control",
+ connectedPlugAppArmor: processControlConnectedPlugAppArmor,
+ connectedPlugSecComp: processControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type ProcessControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&ProcessControlInterfaceSuite{
+ iface: builtin.NewProcessControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "process-control",
+ Interface: "process-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "process-control",
+ Interface: "process-control",
+ },
+ },
+})
+
+func (s *ProcessControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "process-control")
+}
+
+func (s *ProcessControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "process-control",
+ Interface: "process-control",
+ }})
+ c.Assert(err, ErrorMatches, "process-control slots are reserved for the operating system snap")
+}
+
+func (s *ProcessControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *ProcessControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "process-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "process-control"`)
+}
+
+func (s *ProcessControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+)
+
+const pulseaudioConnectedPlugAppArmor = `
+/{run,dev}/shm/pulse-shm-* rwk,
+
+owner /{,var/}run/pulse/ r,
+owner /{,var/}run/pulse/native rwk,
+owner /run/user/[0-9]*/ r,
+owner /run/user/[0-9]*/pulse/ rw,
+`
+
+const pulseaudioConnectedPlugAppArmorDesktop = `
+# Only on desktop do we need access to /etc/pulse for any PulseAudio client
+# to read available client side configuration settings. On an Ubuntu Core
+# device those things will be stored inside the snap directory.
+/etc/pulse/ r,
+/etc/pulse/* r,
+owner @{HOME}/.pulse-cookie rk,
+owner @{HOME}/.config/pulse/cookie rk,
+owner /{,var/}run/user/*/pulse/ rwk,
+owner /{,var/}run/user/*/pulse/native rwk,
+`
+
+const pulseaudioConnectedPlugSecComp = `
+getsockopt
+setsockopt
+connect
+sendto
+shmctl
+getsockname
+getpeername
+sendmsg
+recvmsg
+`
+
+const pulseaudioPermanentSlotAppArmor = `
+# When running PulseAudio in system mode it will switch to the at
+# build time configured user/group on startup.
+capability setuid,
+capability setgid,
+
+capability sys_nice,
+capability sys_resource,
+
+owner @{PROC}/@{pid}/exe r,
+/etc/machine-id r,
+
+# Audio related
+@{PROC}/asound/devices r,
+@{PROC}/asound/card** r,
+
+# Should use the alsa interface instead
+/dev/snd/pcm* rw,
+/dev/snd/control* rw,
+/dev/snd/timer r,
+
+/sys/**/sound/** r,
+
+# For udev
+network netlink raw,
+/sys/devices/virtual/dmi/id/sys_vendor r,
+/sys/devices/virtual/dmi/id/bios_vendor r,
+# FIXME: use udev queries to make this more specific
+/run/udev/data/** r,
+
+owner /{,var/}run/pulse/ rw,
+owner /{,var/}run/pulse/** rwk,
+
+# Shared memory based communication with clients
+/{run,dev}/shm/pulse-shm-* rwk,
+
+/usr/share/applications/ r,
+
+owner /run/pulse/native/ rwk,
+owner /run/user/[0-9]*/ r,
+owner /run/user/[0-9]*/pulse/ rw,
+`
+
+const pulseaudioPermanentSlotSecComp = `
+# The following are needed for UNIX sockets
+personality
+setpriority
+setsockopt
+getsockname
+bind
+listen
+sendto
+recvfrom
+accept4
+shmctl
+getsockname
+getpeername
+sendmsg
+recvmsg
+# Needed to set root as group for different state dirs
+# pulseaudio creates on startup.
+setgroups
+setgroups32
+`
+
+type PulseAudioInterface struct{}
+
+func (iface *PulseAudioInterface) Name() string {
+ return "pulseaudio"
+}
+
+func (iface *PulseAudioInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PulseAudioInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ if release.OnClassic {
+ b := bytes.NewBuffer([]byte(pulseaudioConnectedPlugAppArmor))
+ b.Write([]byte(pulseaudioConnectedPlugAppArmorDesktop))
+ return b.Bytes(), nil
+ } else {
+ return []byte(pulseaudioConnectedPlugAppArmor), nil
+ }
+ case interfaces.SecuritySecComp:
+ return []byte(pulseaudioConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *PulseAudioInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(pulseaudioPermanentSlotAppArmor), nil
+ case interfaces.SecuritySecComp:
+ return []byte(pulseaudioPermanentSlotSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *PulseAudioInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *PulseAudioInterface) SanitizePlug(slot *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *PulseAudioInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *PulseAudioInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type PulseAudioInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&PulseAudioInterfaceSuite{
+ iface: &builtin.PulseAudioInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "pulseaudio"},
+ Name: "pulseaudio",
+ Interface: "pulseaudio",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "pulseaudio",
+ Interface: "pulseaudio",
+ },
+ },
+})
+
+func (s *PulseAudioInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "pulseaudio")
+}
+
+func (s *PulseAudioInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+func (s *PulseAudioInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const rawusbConnectedPlugAppArmor = `
+# Description: Allow raw access to all connected USB devices.
+# Reserved because this gives privileged access to the system.
+# Usage: reserved
+/dev/bus/usb/[0-9][0-9][0-9]/[0-9][0-9][0-9] rw,
+
+# Allow detection of usb devices. Leaks plugged in USB device info
+/sys/bus/usb/devices/ r,
+/sys/devices/pci**/usb[0-9]** r,
+
+/run/udev/data/c16[67]:[0-9] r, # ACM USB modems
+/run/udev/data/b180:* r, # various USB block devices
+/run/udev/data/c18[089]:* r, # various USB character devices: USB serial converters, etc.
+/run/udev/data/+usb:* r,
+`
+
+// Transitional interface which allows access to all usb devices.
+func NewRawUsbInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "raw-usb",
+ connectedPlugAppArmor: rawusbConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type RawUsbSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&RawUsbSuite{
+ iface: builtin.NewRawUsbInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "raw-usb",
+ Interface: "raw-usb",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "raw-usb",
+ Interface: "raw-usb",
+ },
+ },
+})
+
+func (s *RawUsbSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "raw-usb")
+}
+
+func (s *RawUsbSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "raw-usb",
+ Interface: "raw-usb",
+ }})
+ c.Assert(err, ErrorMatches, "raw-usb slots are reserved for the operating system snap")
+}
+
+func (s *RawUsbSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *RawUsbSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "raw-usb"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "raw-usb"`)
+}
+
+func (s *RawUsbSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Assert(string(snippet), testutil.Contains, `/sys/bus/usb/devices/`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const removableMediaConnectedPlugAppArmor = `
+# Description: Can access removable storage filesystems
+
+# Mount points could be in /run/media/<user>/* or /media/<user>/*
+/{,run/}media/*/ r,
+/{,run/}media/*/** rw,
+`
+
+// NewRemovableMediaInterface returns a new "removable-media" interface.
+func NewRemovableMediaInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "removable-media",
+ connectedPlugAppArmor: removableMediaConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type RemovableMediaInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&RemovableMediaInterfaceSuite{
+ iface: builtin.NewRemovableMediaInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "removable-media",
+ Interface: "removable-media",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "removable-media",
+ Interface: "removable-media",
+ },
+ },
+})
+
+func (s *RemovableMediaInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "removable-media")
+}
+
+func (s *RemovableMediaInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "removable-media",
+ Interface: "removable-media",
+ }})
+ c.Assert(err, ErrorMatches, "removable-media slots are reserved for the operating system snap")
+}
+
+func (s *RemovableMediaInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *RemovableMediaInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "removable-media"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "removable-media"`)
+}
+
+func (s *RemovableMediaInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const screenInhibitControlConnectedPlugAppArmor = `
+# Description: Can inhibit and uninhibit screen savers in desktop sessions.
+#include <abstractions/dbus-session-strict>
+#include <abstractions/dbus-strict>
+
+# gnome-session
+dbus (send)
+ bus=session
+ path=/org/gnome/SessionManager
+ interface=org.gnome.SessionManager
+ member={Inhibit,Uninhibit}
+ peer=(label=unconfined),
+
+# unity screen API
+dbus (send)
+ bus=system
+ interface="org.freedesktop.DBus.Introspectable"
+ path="/com/canonical/Unity/Screen"
+ member="Introspect"
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ interface="com.canonical.Unity.Screen"
+ path="/com/canonical/Unity/Screen"
+ member={keepDisplayOn,removeDisplayOnRequest}
+ peer=(label=unconfined),
+
+# freedesktop.org ScreenSaver
+dbus (send)
+ bus=session
+ path=/Screensaver
+ interface=org.freedesktop.ScreenSaver
+ member=org.freedesktop.ScreenSaver.{Inhibit,UnInhibit}
+ peer=(label=unconfined),
+`
+
+const screenInhibitControlConnectedPlugSecComp = `
+# Description: Can inhibit and uninhibit screen savers in desktop sessions.
+# dbus
+connect
+getsockname
+recvfrom
+recvmsg
+send
+sendto
+sendmsg
+socket
+`
+
+// NewScreenInhibitControlInterface returns a new "screen-inhibit-control" interface.
+func NewScreenInhibitControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "screen-inhibit-control",
+ connectedPlugAppArmor: screenInhibitControlConnectedPlugAppArmor,
+ connectedPlugSecComp: screenInhibitControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type ScreenInhibitControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&ScreenInhibitControlInterfaceSuite{
+ iface: builtin.NewScreenInhibitControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "screen-inhibit-control",
+ Interface: "screen-inhibit-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "screen-inhibit-control",
+ Interface: "screen-inhibit-control",
+ },
+ },
+})
+
+func (s *ScreenInhibitControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "screen-inhibit-control")
+}
+
+func (s *ScreenInhibitControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "screen-inhibit-control",
+ Interface: "screen-inhibit-control",
+ }})
+ c.Assert(err, ErrorMatches, "screen-inhibit-control slots are reserved for the operating system snap")
+}
+
+func (s *ScreenInhibitControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *ScreenInhibitControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "screen-inhibit-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "screen-inhibit-control"`)
+}
+
+func (s *ScreenInhibitControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// SerialPortInterface is the type for serial port interfaces.
+type SerialPortInterface struct{}
+
+// Name of the serial-port interface.
+func (iface *SerialPortInterface) Name() string {
+ return "serial-port"
+}
+
+func (iface *SerialPortInterface) String() string {
+ return iface.Name()
+}
+
+// Pattern to match allowed serial device nodes, path attributes will be
+// compared to this for validity when not using udev identification
+var serialDeviceNodePattern = regexp.MustCompile("^/dev/tty[A-Z]{1,3}[0-9]{1,3}$")
+
+// Pattern that is considered valid for the udev symlink to the serial device,
+// path attributes will be compared to this for validity when usb vid and pid
+// are also specified
+var serialUdevSymlinkPattern = regexp.MustCompile("^/dev/serial-port-[a-z0-9]+$")
+
+// SanitizeSlot checks validity of the defined slot
+func (iface *SerialPortInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Check slot is of right type
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // We will only allow creation of this type of slot by a gadget or OS snap
+ if !(slot.Snap.Type == "gadget" || slot.Snap.Type == "os") {
+ return fmt.Errorf("serial-port slots only allowed on gadget or core snaps")
+ }
+
+ // Check slot has a path attribute identify serial device
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return fmt.Errorf("serial-port slot must have a path attribute")
+ }
+
+ // Clean the path before further checks
+ path = filepath.Clean(path)
+
+ if iface.hasUsbAttrs(slot) {
+ // Must be path attribute where symlink will be placed and usb vendor and product identifiers
+ // Check the path attribute is in the allowable pattern
+ if !serialUdevSymlinkPattern.MatchString(path) {
+ return fmt.Errorf("serial-port path attribute specifies invalid symlink location")
+ }
+
+ usbVendor, vOk := slot.Attrs["usb-vendor"].(int64)
+ if !vOk {
+ return fmt.Errorf("serial-port slot failed to find usb-vendor attribute")
+ }
+ if (usbVendor < 0x1) || (usbVendor > 0xFFFF) {
+ return fmt.Errorf("serial-port usb-vendor attribute not valid: %d", usbVendor)
+ }
+
+ usbProduct, pOk := slot.Attrs["usb-product"].(int64)
+ if !pOk {
+ return fmt.Errorf("serial-port slot failed to find usb-product attribute")
+ }
+ if (usbProduct < 0x0) || (usbProduct > 0xFFFF) {
+ return fmt.Errorf("serial-port usb-product attribute not valid: %d", usbProduct)
+ }
+ } else {
+ // Just a path attribute - must be a valid usb device node
+ // Check the path attribute is in the allowable pattern
+ if !serialDeviceNodePattern.MatchString(path) {
+ return fmt.Errorf("serial-port path attribute must be a valid device node")
+ }
+ }
+ return nil
+}
+
+// SanitizePlug checks and possibly modifies a plug.
+func (iface *SerialPortInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // NOTE: currently we don't check anything on the plug side.
+ return nil
+}
+
+// PermanentSlotSnippet returns snippets granted on install
+func (iface *SerialPortInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityUDev:
+ usbVendor, vOk := slot.Attrs["usb-vendor"].(int64)
+ if !vOk {
+ return nil, nil
+ }
+ usbProduct, pOk := slot.Attrs["usb-product"].(int64)
+ if !pOk {
+ return nil, nil
+ }
+ path, ok := slot.Attrs["path"].(string)
+ if !ok || path == "" {
+ return nil, nil
+ }
+ return udevUsbDeviceSnippet("tty", usbVendor, usbProduct, "SYMLINK", strings.TrimPrefix(path, "/dev/")), nil
+ }
+ return nil, nil
+}
+
+// ConnectedSlotSnippet no extra permissions granted on connection
+func (iface *SerialPortInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// PermanentPlugSnippet no permissions provided to plug permanently
+func (iface *SerialPortInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// ConnectedPlugSnippet returns security snippet specific to the plug
+func (iface *SerialPortInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ if iface.hasUsbAttrs(slot) {
+ // This apparmor rule must match serialDeviceNodePattern
+ // UDev tagging and device cgroups will restrict down to the specific device
+ return []byte("/dev/tty[A-Z]{,[A-Z],[A-Z][A-Z]}[0-9]{,[0-9],[0-9][0-9]} rw,\n"), nil
+ }
+
+ // Path to fixed device node (no udev tagging)
+ path, pathOk := slot.Attrs["path"].(string)
+ if !pathOk {
+ return nil, nil
+ }
+ cleanedPath := filepath.Clean(path)
+ return []byte(fmt.Sprintf("%s rw,\n", cleanedPath)), nil
+ case interfaces.SecurityUDev:
+ usbVendor, vOk := slot.Attrs["usb-vendor"].(int64)
+ if !vOk {
+ return nil, nil
+ }
+ usbProduct, pOk := slot.Attrs["usb-product"].(int64)
+ if !pOk {
+ return nil, nil
+ }
+ var udevSnippet bytes.Buffer
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ udevSnippet.Write(udevUsbDeviceSnippet("tty", usbVendor, usbProduct, "TAG", tag))
+ }
+ return udevSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+func (iface *SerialPortInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
+
+func (iface *SerialPortInterface) hasUsbAttrs(slot *interfaces.Slot) bool {
+ if _, ok := slot.Attrs["usb-vendor"]; ok {
+ return true
+ }
+ if _, ok := slot.Attrs["usb-product"]; ok {
+ return true
+ }
+ return false
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type SerialPortInterfaceSuite struct {
+ testutil.BaseTest
+ iface interfaces.Interface
+
+ // OS Snap
+ testSlot1 *interfaces.Slot
+ testSlot2 *interfaces.Slot
+ testSlot3 *interfaces.Slot
+ testSlot4 *interfaces.Slot
+ missingPathSlot *interfaces.Slot
+ badPathSlot1 *interfaces.Slot
+ badPathSlot2 *interfaces.Slot
+ badPathSlot3 *interfaces.Slot
+ badInterfaceSlot *interfaces.Slot
+
+ // Gadget Snap
+ testUdev1 *interfaces.Slot
+ testUdev2 *interfaces.Slot
+ testUdevBadValue1 *interfaces.Slot
+ testUdevBadValue2 *interfaces.Slot
+ testUdevBadValue3 *interfaces.Slot
+
+ // Consuming Snap
+ testPlugPort1 *interfaces.Plug
+ testPlugPort2 *interfaces.Plug
+}
+
+var _ = Suite(&SerialPortInterfaceSuite{
+ iface: &builtin.SerialPortInterface{},
+})
+
+func (s *SerialPortInterfaceSuite) SetUpTest(c *C) {
+ osSnapInfo := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ test-port-1:
+ interface: serial-port
+ path: /dev/ttyS0
+ test-port-2:
+ interface: serial-port
+ path: /dev/ttyAMA2
+ test-port-3:
+ interface: serial-port
+ path: /dev/ttyUSB927
+ test-port-4:
+ interface: serial-port
+ path: /dev/ttyS42
+ missing-path: serial-port
+ bad-path-1:
+ interface: serial-port
+ path: path
+ bad-path-2:
+ interface: serial-port
+ path: /dev/tty0
+ bad-path-3:
+ interface: serial-port
+ path: /dev/ttyUSB9271
+ bad-interface: other-interface
+`, nil)
+ s.testSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-1"]}
+ s.testSlot2 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-2"]}
+ s.testSlot3 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-3"]}
+ s.testSlot4 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-4"]}
+ s.missingPathSlot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["missing-path"]}
+ s.badPathSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-1"]}
+ s.badPathSlot2 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-2"]}
+ s.badPathSlot3 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-3"]}
+ s.badInterfaceSlot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-interface"]}
+
+ gadgetSnapInfo := snaptest.MockInfo(c, `
+name: some-device
+type: gadget
+slots:
+ test-udev-1:
+ interface: serial-port
+ usb-vendor: 0x0001
+ usb-product: 0x0001
+ path: /dev/serial-port-zigbee
+ test-udev-2:
+ interface: serial-port
+ usb-vendor: 0xffff
+ usb-product: 0xffff
+ path: /dev/serial-port-mydevice
+ test-udev-bad-value-1:
+ interface: serial-port
+ usb-vendor: -1
+ usb-product: 0xffff
+ path: /dev/serial-port-mydevice
+ test-udev-bad-value-2:
+ interface: serial-port
+ usb-vendor: 0x1234
+ usb-product: 0x10000
+ path: /dev/serial-port-mydevice
+ test-udev-bad-value-3:
+ interface: serial-port
+ usb-vendor: 0x789a
+ usb-product: 0x4321
+ path: /dev/my-device
+`, nil)
+ s.testUdev1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-1"]}
+ s.testUdev2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-2"]}
+ s.testUdevBadValue1 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-1"]}
+ s.testUdevBadValue2 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-2"]}
+ s.testUdevBadValue3 = &interfaces.Slot{SlotInfo: gadgetSnapInfo.Slots["test-udev-bad-value-3"]}
+
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-port-1:
+ interface: serial-port
+ plug-for-port-2:
+ interface: serial-port
+
+apps:
+ app-accessing-1-port:
+ command: foo
+ plugs: [serial-port]
+ app-accessing-2-ports:
+ command: bar
+ plugs: [plug-for-port-1, plug-for-port-2]
+`, nil)
+ s.testPlugPort1 = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-port-1"]}
+ s.testPlugPort2 = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-port-2"]}
+}
+
+func (s *SerialPortInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "serial-port")
+}
+
+func (s *SerialPortInterfaceSuite) TestSanitizeCoreSnapSlots(c *C) {
+ for _, slot := range []*interfaces.Slot{s.testSlot1, s.testSlot2, s.testSlot3, s.testSlot4} {
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, IsNil)
+ }
+}
+
+func (s *SerialPortInterfaceSuite) TestSanitizeBadCoreSnapSlots(c *C) {
+ // Slots without the "path" attribute are rejected.
+ err := s.iface.SanitizeSlot(s.missingPathSlot)
+ c.Assert(err, ErrorMatches, `serial-port slot must have a path attribute`)
+
+ // Slots with incorrect value of the "path" attribute are rejected.
+ for _, slot := range []*interfaces.Slot{s.badPathSlot1, s.badPathSlot2, s.badPathSlot3} {
+ err := s.iface.SanitizeSlot(slot)
+ c.Assert(err, ErrorMatches, "serial-port path attribute must be a valid device node")
+ }
+
+ // It is impossible to use "bool-file" interface to sanitize slots with other interfaces.
+ c.Assert(func() { s.iface.SanitizeSlot(s.badInterfaceSlot) }, PanicMatches, `slot is not of interface "serial-port"`)
+}
+
+func (s *SerialPortInterfaceSuite) TestSanitizeGadgetSnapSlots(c *C) {
+ err := s.iface.SanitizeSlot(s.testUdev1)
+ c.Assert(err, IsNil)
+
+ err = s.iface.SanitizeSlot(s.testUdev2)
+ c.Assert(err, IsNil)
+}
+
+func (s *SerialPortInterfaceSuite) TestSanitizeBadGadgetSnapSlots(c *C) {
+ err := s.iface.SanitizeSlot(s.testUdevBadValue1)
+ c.Assert(err, ErrorMatches, "serial-port usb-vendor attribute not valid: -1")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue2)
+ c.Assert(err, ErrorMatches, "serial-port usb-product attribute not valid: 65536")
+
+ err = s.iface.SanitizeSlot(s.testUdevBadValue3)
+ c.Assert(err, ErrorMatches, "serial-port path attribute specifies invalid symlink location")
+}
+
+func (s *SerialPortInterfaceSuite) TestPermanentSlotUdevSnippets(c *C) {
+ for _, slot := range []*interfaces.Slot{s.testSlot1, s.testSlot2, s.testSlot3, s.testSlot4} {
+ snippet, err := s.iface.PermanentSlotSnippet(slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ }
+
+ expectedSnippet1 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0001", SYMLINK+="serial-port-zigbee"
+`)
+ snippet, err := s.iface.PermanentSlotSnippet(s.testUdev1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ expectedSnippet2 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="ffff", SYMLINK+="serial-port-mydevice"
+`)
+ snippet, err = s.iface.PermanentSlotSnippet(s.testUdev2, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+}
+
+func (s *SerialPortInterfaceSuite) TestConnectedPlugUdevSnippets(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+
+ expectedSnippet1 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0001", TAG+="snap_client-snap_app-accessing-2-ports"
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ expectedSnippet2 := []byte(`IMPORT{builtin}="usb_id"
+SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="ffff", TAG+="snap_client-snap_app-accessing-2-ports"
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort2, s.testUdev2, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+}
+
+func (s *SerialPortInterfaceSuite) TestConnectedPlugAppArmorSnippets(c *C) {
+ expectedSnippet1 := []byte(`/dev/ttyS0 rw,
+`)
+ snippet, err := s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet))
+
+ expectedSnippet2 := []byte(`/dev/tty[A-Z]{,[A-Z],[A-Z][A-Z]}[0-9]{,[0-9],[0-9][0-9]} rw,
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet))
+
+ expectedSnippet3 := []byte(`/dev/tty[A-Z]{,[A-Z],[A-Z][A-Z]}[0-9]{,[0-9],[0-9][0-9]} rw,
+`)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort2, s.testUdev2, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedSnippet3, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet3, snippet))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const shutdownConnectedPlugAppArmor = `
+# Description: Can reboot, power-off and halt the system.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/systemd1
+ interface=org.freedesktop.systemd1.Manager
+ member={Reboot,PowerOff,Halt}
+ peer=(label=unconfined),
+`
+
+const shutdownConnectedPlugSecComp = `
+# Description: Can reboot, power-off and halt the system.
+# Following things are needed for dbus connectivity
+recvfrom
+recvmsg
+send
+sendto
+sendmsg
+`
+
+// NewShutdownInterface returns a new "shutdown" interface.
+func NewShutdownInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "shutdown",
+ connectedPlugAppArmor: shutdownConnectedPlugAppArmor,
+ connectedPlugSecComp: shutdownConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type ShutdownInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&ShutdownInterfaceSuite{
+ iface: builtin.NewShutdownInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "shutdown",
+ Interface: "shutdown",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "shutdown",
+ Interface: "shutdown",
+ },
+ },
+})
+
+func (s *ShutdownInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "shutdown")
+}
+
+func (s *ShutdownInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "shutdown",
+ Interface: "shutdown",
+ }})
+ c.Assert(err, ErrorMatches, "shutdown slots are reserved for the operating system snap")
+}
+
+func (s *ShutdownInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *ShutdownInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "shutdown"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "shutdown"`)
+}
+
+func (s *ShutdownInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *ShutdownInterfaceSuite) TestConnectedPlugSnippet(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `org.freedesktop.systemd1`)
+
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `recvfrom`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/snapd-control
+const snapdControlConnectedPlugAppArmor = `
+# Description: Can manage snaps via snapd.
+# Usage: reserved
+
+/run/snapd.socket rw,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/snapd-control
+const snapdControlConnectedPlugSecComp = `
+# Description: Can use snapd.
+# Usage: reserved
+
+# Can communicate with snapd abstract socket
+connect
+getsockname
+recv
+recvmsg
+send
+sendto
+sendmsg
+socket
+socketpair
+`
+
+// NewSnapdControlInterface returns a new "snapd-control" interface.
+func NewSnapdControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "snapd-control",
+ connectedPlugAppArmor: snapdControlConnectedPlugAppArmor,
+ connectedPlugSecComp: snapdControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type SnapdControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&SnapdControlInterfaceSuite{
+ iface: builtin.NewSnapdControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "snapd-control",
+ Interface: "snapd-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "snapd-control",
+ Interface: "snapd-control",
+ },
+ },
+})
+
+func (s *SnapdControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "snapd-control")
+}
+
+func (s *SnapdControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "snapd-control",
+ Interface: "snapd-control",
+ }})
+ c.Assert(err, ErrorMatches, "snapd-control slots are reserved for the operating system snap")
+}
+
+func (s *SnapdControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *SnapdControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "snapd-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "snapd-control"`)
+}
+
+func (s *SnapdControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/system-observe
+const systemObserveConnectedPlugAppArmor = `
+# Description: Can query system status information. This is restricted because
+# it gives privileged read access to all processes on the system and should
+# only be used with trusted apps.
+# Usage: reserved
+
+# Needed by 'ps'
+@{PROC}/tty/drivers r,
+
+# This ptrace is an information leak
+ptrace (read),
+
+# ptrace can be used to break out of the seccomp sandbox, but ps requests
+# 'ptrace (trace)' even though it isn't tracing other processes. Unfortunately,
+# this is due to the kernel overloading trace such that the LSMs are unable to
+# distinguish between tracing other processes and other accesses. We deny the
+# trace here to silence the log.
+# Note: for now, explicitly deny to avoid confusion and accidentally giving
+# away this dangerous access frivolously. We may conditionally deny this in the
+# future.
+deny ptrace (trace),
+
+# Other miscellaneous accesses for observing the system
+@{PROC}/stat r,
+@{PROC}/vmstat r,
+@{PROC}/diskstats r,
+@{PROC}/kallsyms r,
+@{PROC}/meminfo r,
+
+# These are not process-specific (/proc/*/... and /proc/*/task/*/...)
+@{PROC}/*/{,task/,task/*/} r,
+@{PROC}/*/{,task/*/}auxv r,
+@{PROC}/*/{,task/*/}cmdline r,
+@{PROC}/*/{,task/*/}exe r,
+@{PROC}/*/{,task/*/}stat r,
+@{PROC}/*/{,task/*/}statm r,
+@{PROC}/*/{,task/*/}status r,
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/system-observe
+const systemObserveConnectedPlugSecComp = `
+# Description: Can query system status information. This is restricted because
+# it gives privileged read access to all processes on the system and should
+# only be used with trusted apps.
+# Usage: reserved
+
+# ptrace can be used to break out of the seccomp sandbox, but ps requests
+# 'ptrace (trace)' from apparmor. 'ps' does not need the ptrace syscall though,
+# so we deny the ptrace here to make sure we are always safe.
+# Note: may uncomment once ubuntu-core-launcher understands @deny rules and
+# if/when we conditionally deny this in the future.
+#@deny ptrace
+
+# for connecting to /org/freedesktop/hostname1 over DBus
+connect
+getsockname
+recvfrom
+recvmsg
+send
+sendto
+sendmsg
+socket
+`
+
+// NewSystemObserveInterface returns a new "system-observe" interface.
+func NewSystemObserveInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "system-observe",
+ connectedPlugAppArmor: systemObserveConnectedPlugAppArmor,
+ connectedPlugSecComp: systemObserveConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type SystemObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&SystemObserveInterfaceSuite{
+ iface: builtin.NewSystemObserveInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "system-observe",
+ Interface: "system-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "system-observe",
+ Interface: "system-observe",
+ },
+ },
+})
+
+func (s *SystemObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "system-observe")
+}
+
+func (s *SystemObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "system-observe",
+ Interface: "system-observe",
+ }})
+ c.Assert(err, ErrorMatches, "system-observe slots are reserved for the operating system snap")
+}
+
+func (s *SystemObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *SystemObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "system-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "system-observe"`)
+}
+
+func (s *SystemObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const systemTraceConnectedPlugAppArmor = `
+# Description: Can use kernel tracing facilities. This is restricted because it
+# gives privileged access to all processes on the system and should only be
+# used with trusted apps.
+# Usage: reserved
+
+ # For the bpf() syscall and manipulating bpf map types
+ capability sys_admin,
+ capability sys_resource,
+
+ # For kernel probes, etc
+ /sys/kernel/debug/kprobes/ r,
+ /sys/kernel/debug/kprobes/** r,
+
+ /sys/kernel/debug/tracing/ r,
+ /sys/kernel/debug/tracing/** rw,
+
+ # Access to kernel headers required for iovisor/bcc. This is typically
+ # detected with 'ls -l /lib/modules/$(uname -r)/build/' which is a symlink
+ # to /usr/src on Ubuntu and so only /usr/src is needed.
+ /usr/src/ r,
+ /usr/src/** r,
+`
+
+const systemTraceConnectedPlugSecComp = `
+# Description: Can use kernel tracing facilities. This is restricted because it
+# gives privileged access to all processes on the system and should only be
+# used with trusted apps.
+# Usage: reserved
+
+bpf
+perf_event_open
+`
+
+// NewSystemTraceInterface returns a new "system-trace" interface.
+func NewSystemTraceInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "system-trace",
+ connectedPlugAppArmor: systemTraceConnectedPlugAppArmor,
+ connectedPlugSecComp: systemTraceConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type SystemTraceInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&SystemTraceInterfaceSuite{
+ iface: builtin.NewSystemTraceInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "system-trace",
+ Interface: "system-trace",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "system-trace",
+ Interface: "system-trace",
+ },
+ },
+})
+
+func (s *SystemTraceInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "system-trace")
+}
+
+func (s *SystemTraceInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "system-trace",
+ Interface: "system-trace",
+ }})
+ c.Assert(err, ErrorMatches, "system-trace slots are reserved for the operating system snap")
+}
+
+func (s *SystemTraceInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *SystemTraceInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "system-trace"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "system-trace"`)
+}
+
+func (s *SystemTraceInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const timeControlConnectedPlugAppArmor = `
+# Description: Can set time and date via systemd' timedated D-Bus interface.
+# Can read all properties of /org/freedesktop/timedate1 D-Bus object; see
+# https://www.freedesktop.org/wiki/Software/systemd/timedated/; This also
+# gives full access to the RTC device nodes and relevant parts of sysfs.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Introspection of org.freedesktop.timedate1
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.timedate1
+ member="Set{Time,LocalRTC}"
+ peer=(label=unconfined),
+
+# Read all properties from timedate1
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+
+# Receive timedate1 property changed events
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=unconfined),
+
+# Allow write access to system real-time clock
+# See 'man 4 rtc' for details.
+
+capability sys_time,
+
+/dev/rtc[0-9]* rw,
+
+# Access to the sysfs nodes are needed by rtcwake for example
+# to program scheduled wakeups in the future.
+/sys/class/rtc/*/ rw,
+/sys/class/rtc/*/** rw,
+
+# As the core snap ships the hwclock utility we can also allow
+# clients to use it now that they have access to the relevant
+# device nodes.
+/sbin/hwclock ixr,
+`
+const timeControlConnectedPlugSecComp = `
+# dbus
+connect
+getsockname
+recvmsg
+recvfrom
+send
+sendto
+sendmsg
+socket
+`
+
+// The type for the rtc interface
+type TimeControlInterface struct{}
+
+// Getter for the name of the rtc interface
+func (iface *TimeControlInterface) Name() string {
+ return "time-control"
+}
+
+func (iface *TimeControlInterface) String() string {
+ return iface.Name()
+}
+
+// Check validity of the defined slot
+func (iface *TimeControlInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ // Does it have right type?
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+
+ // Creation of the slot of this type
+ // is allowed only by a gadget or os snap
+ if !(slot.Snap.Type == "os") {
+ return fmt.Errorf("%s slots are reserved for the operating system snap", iface.Name())
+ }
+ return nil
+}
+
+// Checks and possibly modifies a plug
+func (iface *TimeControlInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface))
+ }
+ // Currently nothing is checked on the plug side
+ return nil
+}
+
+// Returns snippet granted on install
+func (iface *TimeControlInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// Getter for the security snippet specific to the plug
+func (iface *TimeControlInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(timeControlConnectedPlugAppArmor), nil
+
+ case interfaces.SecuritySecComp:
+ return []byte(timeControlConnectedPlugSecComp), nil
+
+ case interfaces.SecurityUDev:
+ var tagSnippet bytes.Buffer
+ const udevRule = `KERNEL=="/dev/rtc0", TAG+="%s"`
+ for appName := range plug.Apps {
+ tag := udevSnapSecurityName(plug.Snap.Name(), appName)
+ tagSnippet.WriteString(fmt.Sprintf(udevRule, tag))
+ }
+ return tagSnippet.Bytes(), nil
+ }
+ return nil, nil
+}
+
+// No extra permissions granted on connection
+func (iface *TimeControlInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+// No permissions granted to plug permanently
+func (iface *TimeControlInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *TimeControlInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // Allow what is allowed in the declarations
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type TimeControlTestInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&TimeControlTestInterfaceSuite{
+ iface: &builtin.TimeControlInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "time-control",
+ Interface: "time-control",
+ },
+ },
+ plug: nil,
+})
+
+func (s *TimeControlTestInterfaceSuite) SetUpTest(c *C) {
+ consumingSnapInfo := snaptest.MockInfo(c, `
+name: client-snap
+plugs:
+ plug-for-time-control:
+ interface: time-control
+apps:
+ app-accessing-time-control:
+ command: foo
+ plugs: [plug-for-time-control]
+`, nil)
+ s.plug = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-time-control"]}
+}
+
+func (s *TimeControlTestInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "time-control")
+}
+
+func (s *TimeControlTestInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "time-control",
+ Interface: "time-control",
+ }})
+ c.Assert(err, ErrorMatches, "time-control slots are reserved for the operating system snap")
+}
+
+func (s *TimeControlTestInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *TimeControlTestInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "time-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "time-control"`)
+}
+
+func (s *TimeControlTestInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ expectedUDevSnippet := []byte(`KERNEL=="/dev/rtc0", TAG+="snap_client-snap_app-accessing-time-control"`)
+
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for udev
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, expectedUDevSnippet, Commentf("\nexpected:\n%s\nfound:\n%s", expectedUDevSnippet, snippet))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/timeserver-control
+const timeserverControlConnectedPlugAppArmor = `
+# Description: Can manage timeservers directly separate from config ubuntu-core.
+# Can enable system clock NTP synchronization via timedated D-Bus interface,
+# Can read all properties of /org/freedesktop/timedate1 D-Bus object; see
+# https://www.freedesktop.org/wiki/Software/systemd/timedated/
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+# Won't work until LP: #1504657 is fixed. Requires reboot until timesyncd
+# notices the change or systemd restarts it.
+/etc/systemd/timesyncd.conf rw,
+
+# Introspection of org.freedesktop.timedate1
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.timedate1
+ member="SetNTP"
+ peer=(label=unconfined),
+
+# Read all properties from timedate1
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+
+# Receive timedate1 property changed events
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=unconfined),
+`
+const timeserverControlConnectedPlugSecComp = `
+# dbus
+connect
+getsockname
+recvmsg
+recvfrom
+send
+sendto
+sendmsg
+socket
+`
+
+// NewTimeserverControlInterface returns a new "timeserver-control" interface.
+func NewTimeserverControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "timeserver-control",
+ connectedPlugAppArmor: timeserverControlConnectedPlugAppArmor,
+ connectedPlugSecComp: timeserverControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type TimeserverControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&TimeserverControlInterfaceSuite{
+ iface: builtin.NewTimeserverControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "timeserver-control",
+ Interface: "timeserver-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "timeserver-control",
+ Interface: "timeserver-control",
+ },
+ },
+})
+
+func (s *TimeserverControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "timeserver-control")
+}
+
+func (s *TimeserverControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "timeserver-control",
+ Interface: "timeserver-control",
+ }})
+ c.Assert(err, ErrorMatches, "timeserver-control slots are reserved for the operating system snap")
+}
+
+func (s *TimeserverControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *TimeserverControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "timeserver-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "timeserver-control"`)
+}
+
+func (s *TimeserverControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/timezone-control
+const timezoneControlConnectedPlugAppArmor = `
+# Description: Can manage timezones directly separate from 'config ubuntu-core'.
+# Can change timezone via timedated D-Bus interface,
+# Can read all properties of /org/freedesktop/timedate1 D-Bus object, see:
+# https://www.freedesktop.org/wiki/Software/systemd/timedated/
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+/usr/share/zoneinfo/ r,
+/usr/share/zoneinfo/** r,
+/etc/{,writable/}timezone rw,
+
+# Introspection of org.freedesktop.timedate1
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.timedate1
+ member="SetTimezone"
+ peer=(label=unconfined),
+
+# Read all properties from timedate1
+dbus (send)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+
+# Receive timedate1 property changed events
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/timedate1
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=unconfined),
+`
+
+const timezoneControlConnectedPlugSecComp = `
+# dbus
+connect
+getsockname
+recvmsg
+recvfrom
+send
+sendto
+sendmsg
+socket
+`
+
+// NewTimezoneControlInterface returns a new "timezone-control" interface.
+func NewTimezoneControlInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "timezone-control",
+ connectedPlugAppArmor: timezoneControlConnectedPlugAppArmor,
+ connectedPlugSecComp: timezoneControlConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type TimezoneControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&TimezoneControlInterfaceSuite{
+ iface: builtin.NewTimezoneControlInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "timezone-control",
+ Interface: "timezone-control",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "timezone-control",
+ Interface: "timezone-control",
+ },
+ },
+})
+
+func (s *TimezoneControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "timezone-control")
+}
+
+func (s *TimezoneControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "timezone-control",
+ Interface: "timezone-control",
+ }})
+ c.Assert(err, ErrorMatches, "timezone-control slots are reserved for the operating system snap")
+}
+
+func (s *TimezoneControlInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *TimezoneControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "timezone-control"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "timezone-control"`)
+}
+
+func (s *TimezoneControlInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import "github.com/snapcore/snapd/interfaces"
+
+const tpmConnectedPlugAppArmor = `
+# Description: for those who need to talk to the system TPM chip over /dev/tpm0
+# Usage: reserved
+
+/dev/tpm0 rw,
+`
+
+func NewTpmInterface() interfaces.Interface {
+ return &commonInterface{
+ name: "tpm",
+ connectedPlugAppArmor: tpmConnectedPlugAppArmor,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type TpmInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&TpmInterfaceSuite{
+ iface: builtin.NewTpmInterface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "tpm",
+ Interface: "tpm",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "tpm",
+ Interface: "tpm",
+ },
+ },
+})
+
+func (s *TpmInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "tpm")
+}
+
+func (s *TpmInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "tpm",
+ Interface: "tpm",
+ }})
+ c.Assert(err, ErrorMatches, "tpm slots are reserved for the operating system snap")
+}
+
+func (s *TpmInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *TpmInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "tpm"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "tpm"`)
+}
+
+func (s *TpmInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const udisks2PermanentSlotAppArmor = `
+# Description: Allow operating as the udisks2. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+# DBus accesses
+#include <abstractions/dbus-strict>
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="{Request,Release}Name"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionUnix{ProcessID,User}"
+ peer=(label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="org.freedesktop.UDisks2",
+
+# Allow unconfined to talk to us. The API for unconfined will be limited
+# with DBus policy, below.
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UDisks2{,/**}
+ interface=org.freedesktop.DBus*
+ peer=(label=unconfined),
+
+# Needed for mount/unmount operations
+capability sys_admin,
+
+# Allow scanning of devices
+network netlink raw,
+/run/udev/data/b[0-9]*:[0-9]* r,
+/sys/devices/**/block/** r,
+
+# Mount points could be in /run/media/<user>/* or /media/<user>/*
+/run/systemd/seats/* r,
+/{,run/}media/{,**} rw,
+mount options=(ro,nosuid,nodev) /dev/{sd*,mmcblk*} -> /{,run/}media/**,
+mount options=(rw,nosuid,nodev) /dev/{sd*,mmcblk*} -> /{,run/}media/**,
+umount /{,run/}media/**,
+
+# This should probably be patched to use $SNAP_DATA/run/...
+/run/udisks2/{,**} rw,
+
+# udisksd execs mount/umount to do the actual operations
+/bin/mount ixr,
+/bin/umount ixr,
+
+# mount/umount (via libmount) track some mount info in these files
+/run/mount/utab* wrl,
+
+# Udisks2 needs to read the raw device for partition information. These rules
+# give raw read access to the system disks and therefore the entire system.
+/dev/sd* r,
+/dev/mmcblk* r,
+`
+
+var udisks2ConnectedSlotAppArmor = []byte(`
+# Allow connected clients to interact with the service. Reserved because this
+# gives privileged access to the system.
+# Usage: reserved
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/UDisks2/**
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UDisks2
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow access to the Udisks2 API
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UDisks2/**
+ interface=org.freedesktop.UDisks2.*
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`)
+
+var udisks2ConnectedPlugAppArmor = []byte(`
+# Description: Allow using udisks service. Reserved because this gives
+# privileged access to the service.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/UDisks2/**
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UDisks2
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Allow access to the Udisks2 API
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UDisks2/**
+ interface=org.freedesktop.UDisks2.*
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`)
+
+const udisks2PermanentSlotSecComp = `
+bind
+chown32
+fchown
+fchown32
+fchownat
+lchown
+lchown32
+getsockname
+setsockopt
+mount
+recv
+recvfrom
+recvmsg
+send
+sendmsg
+sendto
+shmctl
+umount
+umount2
+`
+
+const udisks2ConnectedPlugSecComp = `
+getsockname
+recv
+recvfrom
+recvmsg
+send
+sendmsg
+sendto
+`
+
+const udisks2PermanentSlotDBus = `
+<policy user="root">
+ <allow own="org.freedesktop.UDisks2"/>
+ <allow send_destination="org.freedesktop.UDisks2"/>
+</policy>
+`
+
+const udisks2ConnectedPlugDBus = `
+<policy context="default">
+ <deny own="org.freedesktop.UDisks2"/>
+ <deny send_destination="org.freedesktop.UDisks2"/>
+</policy>
+`
+
+const udisks2PermanentSlotUDev = `
+# These udev rules come from the upstream udisks2 package
+#
+# This file contains udev rules for udisks 2.x
+#
+# Do not edit this file, it will be overwritten on updates
+#
+
+# ------------------------------------------------------------------------
+# Probing
+# ------------------------------------------------------------------------
+
+# Skip probing if not a block device or if requested by other rules
+#
+SUBSYSTEM!="block", GOTO="udisks_probe_end"
+ENV{DM_MULTIPATH_DEVICE_PATH}=="?*", GOTO="udisks_probe_end"
+ENV{DM_UDEV_DISABLE_OTHER_RULES_FLAG}=="?*", GOTO="udisks_probe_end"
+
+# MD-RAID (aka Linux Software RAID) members
+#
+# TODO: file bug against mdadm(8) to have --export-prefix option that can be used with e.g. UDISKS_MD_MEMBER
+#
+SUBSYSTEM=="block", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{UDISKS_MD_MEMBER_LEVEL}=="", IMPORT{program}="/bin/sh -c '/sbin/mdadm --examine --export $tempnode | sed s/^MD_/UDISKS_MD_MEMBER_/g'"
+
+SUBSYSTEM=="block", KERNEL=="md*", ENV{DEVTYPE}!="partition", IMPORT{program}="/bin/sh -c '/sbin/mdadm --detail --export $tempnode | sed s/^MD_/UDISKS_MD_/g'"
+
+LABEL="udisks_probe_end"
+
+# ------------------------------------------------------------------------
+# Tag floppy drives since they need special care
+
+# PC floppy drives
+#
+KERNEL=="fd*", ENV{ID_DRIVE_FLOPPY}="1"
+
+# USB floppy drives
+#
+SUBSYSTEMS=="usb", ATTRS{bInterfaceClass}=="08", ATTRS{bInterfaceSubClass}=="04", ENV{ID_DRIVE_FLOPPY}="1"
+
+# ATA Zip drives
+#
+ENV{ID_VENDOR}=="*IOMEGA*", ENV{ID_MODEL}=="*ZIP*", ENV{ID_DRIVE_FLOPPY_ZIP}="1"
+
+# TODO: figure out if the drive supports SD and SDHC and what the current
+# kind of media is - right now we just assume SD
+KERNEL=="mmcblk[0-9]", SUBSYSTEMS=="mmc", ENV{DEVTYPE}=="disk", ENV{ID_DRIVE_FLASH_SD}="1", ENV{ID_DRIVE_MEDIA_FLASH_SD}="1"
+# ditto for memstick
+KERNEL=="mspblk[0-9]", SUBSYSTEMS=="memstick", ENV{DEVTYPE}=="disk", ENV{ID_DRIVE_FLASH_MS}="1", ENV{ID_DRIVE_MEDIA_FLASH_MS}="1"
+
+# TODO: maybe automatically convert udisks1 properties to udisks2 ones?
+# (e.g. UDISKS_PRESENTATION_HIDE -> UDISKS_IGNORE)
+
+# ------------------------------------------------------------------------
+# ------------------------------------------------------------------------
+# ------------------------------------------------------------------------
+# Whitelist for tagging drives with the property media type.
+# TODO: figure out where to store this database
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0248", ENV{ID_INSTANCE}=="0:0", ENV{ID_DRIVE_FLASH_CF}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0248", ENV{ID_INSTANCE}=="0:1", ENV{ID_DRIVE_FLASH_MS}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0248", ENV{ID_INSTANCE}=="0:2", ENV{ID_DRIVE_FLASH_SM}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="050d", ATTRS{idProduct}=="0248", ENV{ID_INSTANCE}=="0:3", ENV{ID_DRIVE_FLASH_SD}="1"
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="070e", ENV{ID_INSTANCE}=="0:0", ENV{ID_DRIVE_FLASH_CF}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="070e", ENV{ID_INSTANCE}=="0:1", ENV{ID_DRIVE_FLASH_SM}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="070e", ENV{ID_INSTANCE}=="0:2", ENV{ID_DRIVE_FLASH_SD}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="070e", ENV{ID_INSTANCE}=="0:3", ENV{ID_DRIVE_FLASH_MS}="1"
+
+# APPLE SD Card Reader (MacbookPro5,4)
+#
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="05ac", ATTRS{idProduct}=="8403", ENV{ID_DRIVE_FLASH_SD}="1"
+
+# Realtek card readers
+DRIVERS=="rts_pstor", ENV{ID_DRIVE_FLASH_SD}="1"
+DRIVERS=="rts5229", ENV{ID_DRIVE_FLASH_SD}="1"
+
+# Lexar Dual Slot USB 3.0 Reader Professional
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="05dc",ENV{ID_MODEL_ID}=="b049", ENV{ID_INSTANCE}=="0:0", ENV{ID_DRIVE_FLASH_CF}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="05dc",ENV{ID_MODEL_ID}=="b049", ENV{ID_INSTANCE}=="0:1", ENV{ID_DRIVE_FLASH_SD}="1"
+
+# Transcend USB 3.0 Multi-Card Reader (TS-RDF8K)
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="8564",ENV{ID_MODEL_ID}=="4000", ENV{ID_INSTANCE}=="0:0", ENV{ID_DRIVE_FLASH_CF}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="8564",ENV{ID_MODEL_ID}=="4000", ENV{ID_INSTANCE}=="0:1", ENV{ID_DRIVE_FLASH_SD}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="8564",ENV{ID_MODEL_ID}=="4000", ENV{ID_INSTANCE}=="0:2", ENV{ID_DRIVE_FLASH_MS}="1"
+
+# Common theme
+#
+SUBSYSTEMS=="usb", ENV{ID_MODEL}=="*Reader*SD*", ENV{ID_DRIVE_FLASH_SD}="1"
+SUBSYSTEMS=="usb", ENV{ID_MODEL}=="*CF_Reader*", ENV{ID_DRIVE_FLASH_CF}="1"
+SUBSYSTEMS=="usb", ENV{ID_MODEL}=="*SM_Reader*", ENV{ID_DRIVE_FLASH_SM}="1"
+SUBSYSTEMS=="usb", ENV{ID_MODEL}=="*MS_Reader*", ENV{ID_DRIVE_FLASH_MS}="1"
+
+# USB stick / thumb drives
+#
+SUBSYSTEMS=="usb", ENV{ID_VENDOR}=="*Kingston*", ENV{ID_MODEL}=="*DataTraveler*", ENV{ID_DRIVE_THUMB}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR}=="*SanDisk*", ENV{ID_MODEL}=="*Cruzer*", ENV{ID_CDROM}!="1", ENV{ID_DRIVE_THUMB}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR}=="HP", ENV{ID_MODEL}=="*v125w*", ENV{ID_DRIVE_THUMB}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="13fe", ENV{ID_MODEL}=="*Patriot*", ENV{ID_DRIVE_THUMB}="1"
+SUBSYSTEMS=="usb", ENV{ID_VENDOR}=="*JetFlash*", ENV{ID_MODEL}=="*Transcend*", ENV{ID_DRIVE_THUMB}="1"
+
+# SD-Card reader in Chromebook Pixel
+SUBSYSTEMS=="usb", ENV{ID_VENDOR_ID}=="05e3", ENV{ID_MODEL_ID}=="0727", ENV{ID_DRIVE_FLASH_SD}="1"
+
+# ------------------------------------------------------------------------
+# ------------------------------------------------------------------------
+# ------------------------------------------------------------------------
+# Devices which should not be display in the user interface
+#
+# (note that RAID/LVM members are not normally shown in an user
+# interface so setting UDISKS_IGNORE at first does not seem to achieve
+# anything. However it helps for RAID/LVM members that are encrypted
+# using LUKS. See bug #51439.)
+
+# Apple Bootstrap partitions
+ENV{ID_PART_ENTRY_SCHEME}=="mac", ENV{ID_PART_ENTRY_TYPE}=="Apple_Bootstrap", ENV{UDISKS_IGNORE}="1"
+
+# Apple Boot partitions
+ENV{ID_PART_ENTRY_SCHEME}=="gpt", ENV{ID_PART_ENTRY_TYPE}=="426f6f74-0000-11aa-aa11-00306543ecac", ENV{UDISKS_IGNORE}="1"
+
+# special DOS partition types (EFI, hidden, etc.) and RAID/LVM
+# see http://www.win.tue.nl/~aeb/partitions/partition_types-1.html
+ENV{ID_PART_ENTRY_SCHEME}=="dos", \
+ ENV{ID_PART_ENTRY_TYPE}=="0x0|0x11|0x12|0x14|0x16|0x17|0x1b|0x1c|0x1e|0x27|0x3d|0x84|0x8d|0x8e|0x90|0x91|0x92|0x93|0x97|0x98|0x9a|0x9b|0xbb|0xc2|0xc3|0xdd|0xef|0xfd", \
+ ENV{UDISKS_IGNORE}="1"
+
+# special GUID-identified partition types (EFI System Partition, BIOS Boot partition, RAID/LVM)
+# see http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
+ENV{ID_PART_ENTRY_SCHEME}=="gpt", \
+ ENV{ID_PART_ENTRY_TYPE}=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b|21686148-6449-6e6f-744e-656564454649|a19d880f-05fc-4d3b-a006-743f0f84911e|e6d6d379-f507-44c2-a23c-238f2a3df928|e3c9e316-0b5c-4db8-817d-f92df00215ae|de94bba4-06d1-4d40-a16a-bfd50179d6ac", \
+ ENV{UDISKS_IGNORE}="1"
+
+# MAC recovery/tool partitions which are useless on Linux
+ENV{ID_PART_ENTRY_SCHEME}=="mac", \
+ ENV{ID_CDROM}=="?*", ENV{ID_FS_TYPE}=="udf", ENV{ID_FS_LABEL}=="WD*SmartWare", \
+ ENV{UDISKS_IGNORE}="1"
+
+# recovery partitions
+ENV{ID_FS_TYPE}=="ntfs|vfat", \
+ ENV{ID_FS_LABEL}=="Recovery|RECOVERY|Lenovo_Recovery|HP_RECOVERY|Recovery_Partition|DellUtility|DellRestore|IBM_SERVICE|SERVICEV001|SERVICEV002|SYSTEM_RESERVED|System_Reserved|WINRE_DRV|DIAGS|IntelRST", \
+ ENV{UDISKS_IGNORE}="1"
+
+# read-only non-Linux software installer partitions
+ENV{ID_VENDOR}=="Sony", ENV{ID_MODEL}=="PRS*Launcher", ENV{UDISKS_IGNORE}="1"
+
+# non-Linux software
+KERNEL=="sr*", ENV{ID_VENDOR}=="SanDisk", ENV{ID_MODEL}=="Cruzer", ENV{ID_FS_LABEL}=="U3_System", ENV{UDISKS_IGNORE}="1"
+
+# Content created using isohybrid (typically used on CDs and USB
+# sticks for bootable media) is a bit special insofar that the
+# interesting content is on a DOS partition with type 0x00 ... which
+# is hidden above. So undo this.
+#
+# See http://mjg59.dreamwidth.org/11285.html for more details
+#
+ENV{ID_PART_TABLE_TYPE}=="dos", ENV{ID_PART_ENTRY_TYPE}=="0x0", ENV{ID_PART_ENTRY_NUMBER}=="1", ENV{ID_FS_TYPE}=="iso9660|udf", ENV{UDISKS_IGNORE}="0"
+`
+
+type UDisks2Interface struct{}
+
+func (iface *UDisks2Interface) Name() string {
+ return "udisks2"
+}
+
+func (iface *UDisks2Interface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *UDisks2Interface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet := bytes.Replace(udisks2ConnectedPlugAppArmor, old, new, -1)
+ return snippet, nil
+ case interfaces.SecurityDBus:
+ return []byte(udisks2ConnectedPlugDBus), nil
+ case interfaces.SecuritySecComp:
+ return []byte(udisks2ConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *UDisks2Interface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(udisks2PermanentSlotAppArmor), nil
+ case interfaces.SecurityDBus:
+ return []byte(udisks2PermanentSlotDBus), nil
+ case interfaces.SecuritySecComp:
+ return []byte(udisks2PermanentSlotSecComp), nil
+ case interfaces.SecurityUDev:
+ return []byte(udisks2PermanentSlotUDev), nil
+ }
+ return nil, nil
+}
+
+func (iface *UDisks2Interface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace(udisks2ConnectedSlotAppArmor, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *UDisks2Interface) SanitizePlug(slot *interfaces.Plug) error {
+ return nil
+}
+
+func (iface *UDisks2Interface) SanitizeSlot(slot *interfaces.Slot) error {
+ return nil
+}
+
+func (iface *UDisks2Interface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type UDisks2InterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&UDisks2InterfaceSuite{
+ iface: &builtin.UDisks2Interface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "udisks2"},
+ Name: "udisks2",
+ Interface: "udisks2",
+ },
+ },
+})
+
+func (s *UDisks2InterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "udisks2")
+}
+
+func (s *UDisks2InterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+// The label glob when all apps are bound to the udisks2 slot
+func (s *UDisks2InterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.udisks2.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the udisks2 slot
+func (s *UDisks2InterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.udisks2.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the udisks2 slot
+func (s *UDisks2InterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.udisks2.app"),`)
+}
+
+// The label glob when all apps are bound to the udisks2 plug
+func (s *UDisks2InterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.udisks2.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the udisks2 plug
+func (s *UDisks2InterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.udisks2.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the udisks2 plug
+func (s *UDisks2InterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "udisks2",
+ Interface: "udisks2",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.udisks2.app"),`)
+}
+
+func (s *UDisks2InterfaceSuite) TestUsedSecuritySystems(c *C) {
+ systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor,
+ interfaces.SecuritySecComp, interfaces.SecurityDBus}
+ for _, system := range systems {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, system)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityUDev)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/unity7
+const unity7ConnectedPlugAppArmor = `
+# Description: Can access Unity7. Restricted because Unity 7 runs on X and
+# requires access to various DBus services and this environment does not prevent
+# eavesdropping or apps interfering with one another.
+# Usage: reserved
+
+#include <abstractions/dbus-strict>
+#include <abstractions/dbus-session-strict>
+#include <abstractions/X>
+
+#include <abstractions/fonts>
+/var/cache/fontconfig/ r,
+/var/cache/fontconfig/** mr,
+
+# subset of gnome abstraction
+/etc/gnome/defaults.list r,
+/usr/share/gnome/applications/ r,
+/usr/share/applications/mimeinfo.cache r,
+
+/etc/gtk-*/* r,
+/usr/lib{,32,64}/gtk-*/** mr,
+/usr/lib{,32,64}/gdk-pixbuf-*/** mr,
+/usr/lib/@{multiarch}/gtk-*/** mr,
+/usr/lib/@{multiarch}/gdk-pixbuf-*/** mr,
+
+/etc/pango/* r,
+/usr/lib{,32,64}/pango/** mr,
+/usr/lib/@{multiarch}/pango/** mr,
+
+/usr/share/icons/ r,
+/usr/share/icons/** r,
+/usr/share/icons/*/index.theme rk,
+/usr/share/pixmaps/ r,
+/usr/share/pixmaps/** r,
+/usr/share/unity/icons/** r,
+/usr/share/thumbnailer/icons/** r,
+/usr/share/themes/** r,
+
+# Snappy's 'xdg-open' talks to the snapd-xdg-open service which currently works
+# only in environments supporting dbus-send (eg, X11). In the future once
+# snappy's xdg-open supports all snaps images, this access may move to another
+# interface.
+/usr/local/bin/xdg-open ixr,
+/usr/local/share/applications/{,*} r,
+/usr/bin/dbus-send ixr,
+dbus (send)
+ bus=session
+ path=/
+ interface=com.canonical.SafeLauncher
+ member=OpenURL
+ peer=(label=unconfined),
+
+# input methods (ibus)
+# subset of ibus abstraction
+/usr/lib/@{multiarch}/gtk-2.0/[0-9]*/immodules/im-ibus.so mr,
+owner @{HOME}/.config/ibus/ r,
+owner @{HOME}/.config/ibus/bus/ r,
+owner @{HOME}/.config/ibus/bus/* r,
+
+# allow communicating with ibus-daemon (this allows sniffing key events)
+unix (connect, receive, send)
+ type=stream
+ peer=(addr="@/tmp/ibus/dbus-*"),
+
+
+# input methods (mozc)
+# allow communicating with mozc server (TODO: investigate if allows sniffing)
+unix (connect, receive, send)
+ type=stream
+ peer=(addr="@tmp/.mozc.*"),
+
+
+# input methods (fcitx)
+# allow communicating with fcitx dbus service
+dbus send
+ bus=fcitx
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Hello,AddMatch,RemoveMatch,GetNameOwner,NameHasOwner,StartServiceByName}
+ peer=(name=org.freedesktop.DBus),
+
+owner @{HOME}/.config/fcitx/dbus/* r,
+
+# allow creating an input context
+dbus send
+ bus={fcitx,session}
+ path=/inputmethod
+ interface=org.fcitx.Fcitx.InputMethod
+ member=CreateIC*
+ peer=(label=unconfined),
+
+# allow setting up and tearing down the input context
+dbus send
+ bus={fcitx,session}
+ path=/inputcontext_[0-9]*
+ interface=org.fcitx.Fcitx.InputContext
+ member="{Close,Destroy,Enable}IC"
+ peer=(label=unconfined),
+
+dbus send
+ bus={fcitx,session}
+ path=/inputcontext_[0-9]*
+ interface=org.fcitx.Fcitx.InputContext
+ member=Reset
+ peer=(label=unconfined),
+
+# allow service to send us signals
+dbus receive
+ bus=fcitx
+ peer=(label=unconfined),
+
+dbus receive
+ bus=session
+ interface=org.fcitx.Fcitx.*
+ peer=(label=unconfined),
+
+# use the input context
+dbus send
+ bus={fcitx,session}
+ path=/inputcontext_[0-9]*
+ interface=org.fcitx.Fcitx.InputContext
+ member="Focus{In,Out}"
+ peer=(label=unconfined),
+
+dbus send
+ bus={fcitx,session}
+ path=/inputcontext_[0-9]*
+ interface=org.fcitx.Fcitx.InputContext
+ member="{CommitPreedit,Set*}"
+ peer=(label=unconfined),
+
+# this is an information leak and allows key and mouse sniffing. If the input
+# context path were tied to the process' security label, this would not be an
+# issue.
+dbus send
+ bus={fcitx,session}
+ path=/inputcontext_[0-9]*
+ interface=org.fcitx.Fcitx.InputContext
+ member="{MouseEvent,ProcessKeyEvent}"
+ peer=(label=unconfined),
+
+# this method does not exist with the sunpinyin backend (at least), so allow
+# it for other input methods. This may consitute an information leak (which,
+# again, could be avoided if the path were tied to the process' security
+# label).
+dbus send
+ bus={fcitx,session}
+ path=/inputcontext_[0-9]*
+ interface=org.freedesktop.DBus.Properties
+ member=GetAll
+ peer=(label=unconfined),
+
+
+# subset of freedesktop.org
+/usr/share/mime/** r,
+owner @{HOME}/.local/share/mime/** r,
+owner @{HOME}/.config/user-dirs.dirs r,
+
+# accessibility
+#include <abstractions/dbus-accessibility-strict>
+dbus (send)
+ bus=session
+ path=/org/a11y/bus
+ interface=org.a11y.Bus
+ member=GetAddress
+ peer=(label=unconfined),
+
+# unfortunate, but org.a11y.atspi is not designed for separation
+dbus (receive, send)
+ bus=accessibility
+ path=/org/a11y/atspi/**
+ peer=(label=unconfined),
+
+# org.freedesktop.Accounts
+dbus (send)
+ bus=system
+ path=/org/freedesktop/Accounts
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/Accounts
+ interface=org.freedesktop.Accounts
+ member=FindUserById
+ peer=(label=unconfined),
+
+# Get() is an information leak
+# TODO: verify what it is leaking
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/Accounts/User[0-9]*
+ interface=org.freedesktop.DBus.Properties
+ member={Get,PropertiesChanged}
+ peer=(label=unconfined),
+
+# gmenu
+# Note: the gmenu DBus api was not designed for application isolation and apps
+# may specify anything as their 'path'. For example, these work in the many
+# cases:
+# - /org/gtk/Application/anonymous{,/**}
+# - /com/canonical/unity/gtk/window/[0-9]*
+# but libreoffice does:
+# - /org/libreoffice{,/**}
+# As such, cannot mediate by DBus path so we'll be as strict as we can in the
+# other mediated parts
+dbus (send)
+ bus=session
+ interface=org.gtk.Actions
+ member=Changed
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (receive)
+ bus=session
+ interface=org.gtk.Actions
+ member={Activate,DescribeAll,SetState}
+ peer=(label=unconfined),
+
+dbus (receive)
+ bus=session
+ interface=org.gtk.Menus
+ member={Start,End}
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=session
+ interface=org.gtk.Menus
+ member=Changed
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# Ubuntu menus
+dbus (send)
+ bus=session
+ path="/com/ubuntu/MenuRegistrar"
+ interface="com.ubuntu.MenuRegistrar"
+ member="{Register,Unregister}{App,Surface}Menu"
+ peer=(label=unconfined),
+
+# url helper
+dbus (send)
+ bus=session
+ interface=com.canonical.SafeLauncher.OpenURL
+ peer=(label=unconfined),
+
+# dbusmenu
+dbus (send)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*}
+ interface=com.canonical.dbusmenu
+ member="{LayoutUpdated,ItemsPropertiesUpdated}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (receive)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*}
+ interface="{com.canonical.dbusmenu,org.freedesktop.DBus.Properties}"
+ member=Get*
+ peer=(label=unconfined),
+
+dbus (receive)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*}
+ interface=com.canonical.dbusmenu
+ member="{AboutTo*,Event*}"
+ peer=(label=unconfined),
+
+# notifications
+dbus (send)
+ bus=session
+ path=/StatusNotifierWatcher
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(name=org.kde.StatusNotifierWatcher, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="{GetConnectionUnixProcessID,RequestName,ReleaseName}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (bind)
+ bus=session
+ name=org.kde.StatusNotifierItem-[0-9]*,
+
+dbus (send)
+ bus=session
+ path=/StatusNotifierWatcher
+ interface=org.freedesktop.DBus.Properties
+ member=Get
+ peer=(name=org.kde.StatusNotifierWatcher, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/{StatusNotifierWatcher,org/ayatana/NotificationItem/*}
+ interface=org.kde.StatusNotifierWatcher
+ member=RegisterStatusNotifierItem
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/{StatusNotifierItem,org/ayatana/NotificationItem/*}
+ interface=org.kde.StatusNotifierItem
+ member="New{AttentionIcon,Icon,OverlayIcon,Status,Title,ToolTip}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/{StatusNotifierItem/menu,org/ayatana/NotificationItem/*/Menu}
+ interface=com.canonical.dbusmenu
+ member="{LayoutUpdated,ItemsPropertiesUpdated}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (receive)
+ bus=session
+ path=/{StatusNotifierItem,StatusNotifierItem/menu,org/ayatana/NotificationItem/**}
+ interface={org.freedesktop.DBus.Properties,com.canonical.dbusmenu}
+ member={Get*,AboutTo*,Event*}
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/org/freedesktop/Notifications
+ interface=org.freedesktop.Notifications
+ member="{GetCapabilities,GetServerInformation,Notify}"
+ peer=(label=unconfined),
+
+dbus (receive)
+ bus=session
+ path=/org/freedesktop/Notifications
+ interface=org.freedesktop.Notifications
+ member=NotificationClosed
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/org/ayatana/NotificationItem/*
+ interface=org.kde.StatusNotifierItem
+ member=XAyatanaNew*
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# unity launcher
+dbus (send)
+ bus=session
+ path=/com/canonical/unity/launcherentry/[0-9]*
+ interface=com.canonical.Unity.LauncherEntry
+ member=Update
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/com/canonical/unity/launcherentry/[0-9]*
+ interface=com.canonical.dbusmenu
+ member="{LayoutUpdated,ItemsPropertiesUpdated}"
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (receive)
+ bus=session
+ path=/com/canonical/unity/launcherentry/[0-9]*
+ interface="{com.canonical.dbusmenu,org.freedesktop.DBus.Properties}"
+ member=Get*
+ peer=(label=unconfined),
+
+# This rule is meant to be covered by abstractions/dbus-session-strict but
+# the unity launcher code has a typo that uses /org/freedesktop/dbus as the
+# path instead of /org/freedesktop/DBus, so we need to all it here.
+dbus (send)
+ bus=session
+ path=/org/freedesktop/dbus
+ interface=org.freedesktop.DBus
+ member=NameHasOwner
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+# appmenu
+dbus (send)
+ bus=session
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member=ListNames
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/com/canonical/AppMenu/Registrar
+ interface=com.canonical.AppMenu.Registrar
+ member="{RegisterWindow,UnregisterWindow}"
+ peer=(label=unconfined),
+
+dbus (send)
+ bus=session
+ path=/com/canonical/AppMenu/Registrar
+ interface=com.canonical.dbusmenu
+ member=UnregisterWindow
+ peer=(label=unconfined),
+
+dbus (receive)
+ bus=session
+ path=/com/canonical/menu/[0-9]*
+ interface="{org.freedesktop.DBus.Properties,com.canonical.dbusmenu}"
+ member="{GetAll,GetLayout}"
+ peer=(label=unconfined),
+
+
+# Lttng tracing is very noisy and should not be allowed by confined apps. Can
+# safely deny. LP: #1260491
+deny /{,var/}{dev,run}/shm/lttng-ust-* r,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/unity7
+const unity7ConnectedPlugSecComp = `
+# Description: Can access Unity7. Restricted because Unity 7 runs on X and
+# requires access to various DBus services and this environment does not prevent
+# eavesdropping or apps interfering with one another.
+
+# X
+getpeername
+recvfrom
+recvmsg
+shutdown
+getsockopt
+
+# dbus
+connect
+getsockname
+recvmsg
+send
+sendto
+sendmsg
+socket
+`
+
+// NewUnity7Interface returns a new "unity7" interface.
+func NewUnity7Interface() interfaces.Interface {
+ return &commonInterface{
+ name: "unity7",
+ connectedPlugAppArmor: unity7ConnectedPlugAppArmor,
+ connectedPlugSecComp: unity7ConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type Unity7InterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&Unity7InterfaceSuite{
+ iface: builtin.NewUnity7Interface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "unity7",
+ Interface: "unity7",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "unity7",
+ Interface: "unity7",
+ },
+ },
+})
+
+func (s *Unity7InterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "unity7")
+}
+
+func (s *Unity7InterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "unity7",
+ Interface: "unity7",
+ }})
+ c.Assert(err, ErrorMatches, "unity7 slots are reserved for the operating system snap")
+}
+
+func (s *Unity7InterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *Unity7InterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "unity7"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "unity7"`)
+}
+
+func (s *Unity7InterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+const upowerObservePermanentSlotAppArmor = `
+# Description: Allow operating as the UPower service.
+
+network netlink raw,
+
+# DBus accesses
+#include <abstractions/dbus-strict>
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={Request,Release}Name
+ peer=(name=org.freedesktop.DBus),
+
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member="GetConnectionUnix{ProcessID,User}"
+ peer=(label=unconfined),
+
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="org.freedesktop.UPower",
+
+# Allow read-only access to service properties
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/login1{,/**}
+ interface=org.freedesktop.DBus.Properties
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ path=/org/freedesktop/login1{,/**}
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+
+# Allow receiving any signals from the logind service
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/login1{,/**}
+ interface=org.freedesktop.login1.*
+ peer=(label=unconfined),
+
+# Allow access to logind service as we need to query it for possible
+# power states and trigger these when the battery gets low and the
+# system enters a critical state.
+dbus (send)
+ bus=system
+ path=/org/freedesktop/login1{,/**}
+ interface=org.freedesktop.login1.Manager
+ member={CanPowerOff,CanSuspend,CanHibernate,CanHybridSleep,PowerOff,Suspend,Hibernate,HybrisSleep}
+ peer=(label=unconfined),
+`
+
+const upowerObserveConnectedSlotAppArmor = `
+# Allow traffic to/from our path and interface with any method
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UPower{,/**}
+ interface=org.freedesktop.UPower*
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# Allow traffic to/from org.freedesktop.DBus for the UPower service
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/UPower{,/**}
+ interface=org.freedesktop.DBus.*
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`
+
+const upowerObservePermanentSlotSeccomp = `
+bind
+recvmsg
+sendmsg
+sendto
+recvfrom
+`
+
+const upowerObservePermanentSlotDBus = `
+<!-- DBus policy for upower (based on upstream version 0.99.4) -->
+<policy user="root">
+ <allow own="org.freedesktop.UPower"/>
+</policy>
+<policy context="default">
+ <deny own="org.freedesktop.UPower"/>
+
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.DBus.Introspectable"/>
+
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.DBus.Peer"/>
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.DBus.Properties"/>
+ <allow send_destination="org.freedesktop.UPower.Device"
+ send_interface="org.freedesktop.DBus.Properties"/>
+ <allow send_destination="org.freedesktop.UPower.KbdBacklight"
+ send_interface="org.freedesktop.DBus.Properties"/>
+ <allow send_destination="org.freedesktop.UPower.Wakeups"
+ send_interface="org.freedesktop.DBus.Properties"/>
+
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.UPower"/>
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.UPower.Device"/>
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.UPower.KbdBacklight"/>
+ <allow send_destination="org.freedesktop.UPower"
+ send_interface="org.freedesktop.UPower.Wakeups"/>
+</policy>
+`
+
+const upowerObserveConnectedPlugAppArmor = `
+# Description: Can query UPower for power devices, history and statistics.
+
+#include <abstractions/dbus-strict>
+
+# Find all devices monitored by UPower
+dbus (send)
+ bus=system
+ path=/org/freedesktop/UPower
+ interface=org.freedesktop.UPower
+ member=EnumerateDevices
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Read all properties from UPower and devices
+dbus (send)
+ bus=system
+ path=/org/freedesktop/UPower{,/devices/**}
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/UPower/Wakeups
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/UPower
+ interface=org.freedesktop.UPower
+ member=GetCriticalAction
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+dbus (send)
+ bus=system
+ path=/org/freedesktop/UPower/devices/**
+ interface=org.freedesktop.UPower.Device
+ member=GetHistory
+ peer=(label=###SLOT_SECURITY_TAGS###),
+
+# Receive property changed events
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/UPower{,/devices/**}
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+const upowerObserveConnectedPlugSecComp = `
+# Description: Can query UPower for power devices, history and statistics.
+
+# dbus
+connect
+getsockname
+recvfrom
+recvmsg
+send
+sendto
+sendmsg
+socket
+`
+
+type UpowerObserveInterface struct{}
+
+func (iface *UpowerObserveInterface) Name() string {
+ return "upower-observe"
+}
+
+func (iface *UpowerObserveInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *UpowerObserveInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ if release.OnClassic {
+ // Let confined apps access unconfined upower on classic
+ new = []byte("unconfined")
+ }
+ snippet := bytes.Replace([]byte(upowerObserveConnectedPlugAppArmor), old, new, -1)
+ return snippet, nil
+ case interfaces.SecuritySecComp:
+ return []byte(upowerObserveConnectedPlugSecComp), nil
+ }
+ return nil, nil
+}
+
+func (iface *UpowerObserveInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityDBus:
+ return []byte(upowerObservePermanentSlotDBus), nil
+ case interfaces.SecurityAppArmor:
+ return []byte(upowerObservePermanentSlotAppArmor), nil
+ case interfaces.SecuritySecComp:
+ return []byte(upowerObservePermanentSlotSeccomp), nil
+ }
+ return nil, nil
+}
+
+func (iface *UpowerObserveInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ old := []byte("###PLUG_SECURITY_TAGS###")
+ new := plugAppLabelExpr(plug)
+ snippet := bytes.Replace([]byte(upowerObserveConnectedSlotAppArmor), old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *UpowerObserveInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *UpowerObserveInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface.Name()))
+ }
+ if slot.Snap.Type != snap.TypeApp && slot.Snap.Type != snap.TypeOS {
+ return fmt.Errorf("%s slots are reserved for the operating system or application snaps", iface.Name())
+ }
+ return nil
+}
+
+func (iface *UpowerObserveInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type UPowerObserveInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&UPowerObserveInterfaceSuite{
+ iface: &builtin.UpowerObserveInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "upower-observe",
+ Interface: "upower-observe",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "upower-observe",
+ Interface: "upower-observe",
+ },
+ },
+})
+
+func (s *UPowerObserveInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "upower-observe")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "upower-observe",
+ Interface: "upower-observe",
+ }})
+ c.Assert(err, ErrorMatches, "upower-observe slots are reserved for the operating system or application snaps")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *UPowerObserveInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "upower-observe"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "upower-observe"`)
+}
+
+func (s *UPowerObserveInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+// The label glob when all apps are bound to the ofono slot
+func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "upower",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ Name: "upower",
+ Interface: "upower-observe",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.upower.*"),`)
+}
+
+// The label uses alternation when some, but not all, apps is bound to the ofono slot
+func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) {
+ app1 := &snap.AppInfo{Name: "app1"}
+ app2 := &snap.AppInfo{Name: "app2"}
+ app3 := &snap.AppInfo{Name: "app3"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "upower",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3},
+ },
+ Name: "upower",
+ Interface: "upower",
+ Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.upower.{app1,app2}"),`)
+}
+
+// The label uses short form when exactly one app is bound to the upower-observe slot
+func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{
+ SuggestedName: "upower",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "upower",
+ Interface: "upower",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.upower.app"),`)
+}
+
+func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetUsesUnconfinedLabelOnClassic(c *C) {
+ release.OnClassic = true
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ // verify apparmor connected
+ c.Assert(string(snippet), testutil.Contains, "#include <abstractions/dbus-strict>")
+ // verify classic connected
+ c.Assert(string(snippet), testutil.Contains, "peer=(label=unconfined),")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetAppArmor(c *C) {
+ release.OnClassic = false
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ // verify apparmor connected
+ c.Assert(string(snippet), testutil.Contains, "#include <abstractions/dbus-strict>")
+ // verify classic didn't connect
+ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "send\n")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestPermanentSlotSnippetAppArmor(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "org.freedesktop.UPower")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestPermanentSlotSnippetSecComp(c *C) {
+ snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ c.Check(string(snippet), testutil.Contains, "bind\n")
+}
+
+func (s *UPowerObserveInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelOne(c *C) {
+ app := &snap.AppInfo{Name: "app"}
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{
+ SuggestedName: "upower",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ Name: "upower",
+ Interface: "upower-observe",
+ Apps: map[string]*snap.AppInfo{"app": app},
+ },
+ }
+ snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.upower.app"),`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+// AppLabelExpr returns the specification of the apparmor label describing
+// all the apps bound to a given slot. The result has one of three forms,
+// depending on how apps are bound to the slot:
+//
+// - "snap.$snap.$app" if there is exactly one app bound
+// - "snap.$snap.{$app1,...$appN}" if there are some, but not all, apps bound
+// - "snap.$snap.*" if all apps are bound to the slot
+func appLabelExpr(apps map[string]*snap.AppInfo, snap *snap.Info) []byte {
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, `"snap.%s.`, snap.Name())
+ if len(apps) == 1 {
+ for appName := range apps {
+ buf.WriteString(appName)
+ }
+ } else if len(apps) == len(snap.Apps) {
+ buf.WriteByte('*')
+ } else {
+ appNames := make([]string, 0, len(apps))
+ for appName := range apps {
+ appNames = append(appNames, appName)
+ }
+ sort.Strings(appNames)
+ buf.WriteByte('{')
+ for _, appName := range appNames {
+ buf.WriteString(appName)
+ buf.WriteByte(',')
+ }
+ buf.Truncate(buf.Len() - 1)
+ buf.WriteByte('}')
+ }
+ buf.WriteByte('"')
+ return buf.Bytes()
+}
+
+func slotAppLabelExpr(slot *interfaces.Slot) []byte {
+ return appLabelExpr(slot.Apps, slot.Snap)
+}
+
+func plugAppLabelExpr(plug *interfaces.Plug) []byte {
+ return appLabelExpr(plug.Apps, plug.Snap)
+}
+
+// Function to support creation of udev snippet
+func udevUsbDeviceSnippet(subsystem string, usbVendor int64, usbProduct int64, key string, data string) []byte {
+ const udevHeader string = `IMPORT{builtin}="usb_id"`
+ const udevDevicePrefix string = `SUBSYSTEM=="%s", SUBSYSTEMS=="usb", ATTRS{idVendor}=="%04x", ATTRS{idProduct}=="%04x"`
+ const udevSuffix string = `, %s+="%s"`
+
+ var udevSnippet bytes.Buffer
+ udevSnippet.WriteString(udevHeader + "\n")
+ udevSnippet.WriteString(fmt.Sprintf(udevDevicePrefix, subsystem, usbVendor, usbProduct))
+ udevSnippet.WriteString(fmt.Sprintf(udevSuffix, key, data))
+ udevSnippet.WriteString("\n")
+ return udevSnippet.Bytes()
+}
+
+// Function to create an udev TAG, essentially the cgroup name for
+// the snap application.
+// @param snapName is the name of the snap
+// @param appName is the name of the application
+// @return string "snap_<snap name>_<app name>"
+func udevSnapSecurityName(snapName string, appName string) string {
+ return fmt.Sprintf(`snap_%s_%s`, snapName, appName)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/x
+const x11ConnectedPlugAppArmor = `
+# Description: Can access the X server. Restricted because X does not prevent
+# eavesdropping or apps interfering with one another.
+# Usage: reserved
+
+#include <abstractions/X>
+#include <abstractions/fonts>
+
+/var/cache/fontconfig/ r,
+/var/cache/fontconfig/** mr,
+`
+
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/x
+const x11ConnectedPlugSecComp = `
+# Description: Can access the X server. Restricted because X does not prevent
+# eavesdropping or apps interfering with one another.
+# Usage: reserved
+
+getpeername
+getsockname
+getsockopt
+recvfrom
+recvmsg
+sendmsg
+shutdown
+`
+
+// NewX11Interface returns a new "x11" interface.
+func NewX11Interface() interfaces.Interface {
+ return &commonInterface{
+ name: "x11",
+ connectedPlugAppArmor: x11ConnectedPlugAppArmor,
+ connectedPlugSecComp: x11ConnectedPlugSecComp,
+ reservedForOS: true,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type X11InterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&X11InterfaceSuite{
+ iface: builtin.NewX11Interface(),
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "x11",
+ Interface: "x11",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "other"},
+ Name: "x11",
+ Interface: "x11",
+ },
+ },
+})
+
+func (s *X11InterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "x11")
+}
+
+func (s *X11InterfaceSuite) TestSanitizeSlot(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+ err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "x11",
+ Interface: "x11",
+ }})
+ c.Assert(err, ErrorMatches, "x11 slots are reserved for the operating system snap")
+}
+
+func (s *X11InterfaceSuite) TestSanitizePlug(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+func (s *X11InterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "x11"`)
+ c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "x11"`)
+}
+
+func (s *X11InterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected plugs have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // connected plugs have a non-nil security snippet for seccomp
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+// The getsockname system call is allowed
+func (s *X11InterfaceSuite) TestLP1574526(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Check(string(snippet), testutil.Contains, "getsockname\n")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// Plug represents the potential of a given snap to connect to a slot.
+type Plug struct {
+ *snap.PlugInfo
+ Connections []SlotRef `json:"connections,omitempty"`
+}
+
+// Ref returns reference to a plug
+func (plug *Plug) Ref() PlugRef {
+ return PlugRef{Snap: plug.Snap.Name(), Name: plug.Name}
+}
+
+// PlugRef is a reference to a plug.
+type PlugRef struct {
+ Snap string `json:"snap"`
+ Name string `json:"plug"`
+}
+
+// Slot represents a capacity offered by a snap.
+type Slot struct {
+ *snap.SlotInfo
+ Connections []PlugRef `json:"connections,omitempty"`
+}
+
+// Ref returns reference to a slot
+func (slot *Slot) Ref() SlotRef {
+ return SlotRef{Snap: slot.Snap.Name(), Name: slot.Name}
+}
+
+// SlotRef is a reference to a slot.
+type SlotRef struct {
+ Snap string `json:"snap"`
+ Name string `json:"slot"`
+}
+
+// Interfaces holds information about a list of plugs and slots, and their connections.
+type Interfaces struct {
+ Plugs []*Plug `json:"plugs"`
+ Slots []*Slot `json:"slots"`
+}
+
+// ConnRef holds information about plug and slot reference that form a particular connection.
+type ConnRef struct {
+ PlugRef PlugRef
+ SlotRef SlotRef
+}
+
+// ID returns a string identifying a given connection.
+func (conn *ConnRef) ID() string {
+ return fmt.Sprintf("%s:%s %s:%s", conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name)
+}
+
+// Interface describes a group of interchangeable capabilities with common features.
+// Interfaces act as a contract between system builders, application developers
+// and end users.
+type Interface interface {
+ // Unique and public name of this interface.
+ Name() string
+
+ // SanitizePlug checks if a plug is correct, altering if necessary.
+ SanitizePlug(plug *Plug) error
+
+ // SanitizeSlot checks if a slot is correct, altering if necessary.
+ SanitizeSlot(slot *Slot) error
+
+ // PermanentPlugSnippet returns the snippet of text for the given security
+ // system that is used during the whole lifetime of affected applications,
+ // whether the plug is connected or not.
+ //
+ // Permanent security snippet can be used to grant permissions to a snap that
+ // has a plug of a given interface even before the plug is connected to a
+ // slot.
+ //
+ // An empty snippet is returned when there are no additional permissions
+ // that are required to implement this interface or when the interface
+ // doesn't recognize the security system.
+ PermanentPlugSnippet(plug *Plug, securitySystem SecuritySystem) ([]byte, error)
+
+ // ConnectedPlugSnippet returns the snippet of text for the given security
+ // system that is used by affected application, while a specific connection
+ // between a plug and a slot exists.
+ //
+ // Connection-specific security snippet can be used to grant permission to
+ // a snap that has a plug of a given interface connected to a slot in
+ // another snap.
+ //
+ // The snippet should be specific to both the plug and the slot. If the
+ // slot is not necessary then consider using PermanentPlugSnippet()
+ // instead.
+ //
+ // An empty snippet is returned when there are no additional permissions
+ // that are required to implement this interface or when the interface
+ // doesn't recognize the security system.
+ ConnectedPlugSnippet(plug *Plug, slot *Slot, securitySystem SecuritySystem) ([]byte, error)
+
+ // PermanentSlotSnippet returns the snippet of text for the given security
+ // system that is used during the whole lifetime of affected applications,
+ // whether the slot is connected or not.
+ //
+ // Permanent security snippet can be used to grant permissions to a snap that
+ // has a slot of a given interface even before the first connection to that
+ // slot is made.
+ //
+ // An empty snippet is returned when there are no additional permissions
+ // that are required to implement this interface or when the interface
+ // doesn't recognize the security system.
+ PermanentSlotSnippet(slot *Slot, securitySystem SecuritySystem) ([]byte, error)
+
+ // ConnectedSlotSnippet returns the snippet of text for the given security
+ // system that is used by affected application, while a specific connection
+ // between a plug and a slot exists.
+ //
+ // Connection-specific security snippet can be used to grant permission to
+ // a snap that has a slot of a given interface connected to a plug in
+ // another snap.
+ //
+ // The snippet should be specific to both the plug and the slot, if the
+ // plug is not necessary then consider using PermanentSlotSnippet()
+ // instead.
+ //
+ // An empty snippet is returned when there are no additional permissions
+ // that are required to implement this interface or when the interface
+ // doesn't recognize the security system.
+ ConnectedSlotSnippet(plug *Plug, slot *Slot, securitySystem SecuritySystem) ([]byte, error)
+
+ // AutoConnect returns whether plug and slot should be
+ // implicitly auto-connected assuming they will be an
+ // unambiguous connection candidate and declaration-based checks
+ // allow.
+ AutoConnect(plug *Plug, slot *Slot) bool
+}
+
+// SecuritySystem is a name of a security system.
+type SecuritySystem string
+
+const (
+ // SecurityAppArmor identifies the apparmor security system.
+ SecurityAppArmor SecuritySystem = "apparmor"
+ // SecuritySecComp identifies the seccomp security system.
+ SecuritySecComp SecuritySystem = "seccomp"
+ // SecurityDBus identifies the DBus security system.
+ SecurityDBus SecuritySystem = "dbus"
+ // SecurityUDev identifies the UDev security system.
+ SecurityUDev SecuritySystem = "udev"
+ // SecurityMount identifies the mount security system.
+ SecurityMount SecuritySystem = "mount"
+ // SecurityKMod identifies the kernel modules security system
+ SecurityKMod SecuritySystem = "kmod"
+ // SecuritySystemd identifies the systemd services security system
+ SecuritySystemd SecuritySystem = "systemd"
+)
+
+// Regular expression describing correct identifiers.
+var validName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$")
+
+// ValidateName checks if a string can be used as a plug or slot name.
+func ValidateName(name string) error {
+ valid := validName.MatchString(name)
+ if !valid {
+ return fmt.Errorf("invalid interface name: %q", name)
+ }
+ return nil
+}
+
+// ValidateDBusBusName checks if a string conforms to
+// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
+func ValidateDBusBusName(busName string) error {
+ if len(busName) == 0 {
+ return fmt.Errorf("DBus bus name must be set")
+ } else if len(busName) > 255 {
+ return fmt.Errorf("DBus bus name is too long (must be <= 255)")
+ }
+
+ validBusName := regexp.MustCompile("^[a-zA-Z_-][a-zA-Z0-9_-]*(\\.[a-zA-Z_-][a-zA-Z0-9_-]*)+$")
+ if !validBusName.MatchString(busName) {
+ return fmt.Errorf("invalid DBus bus name: %q", busName)
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type CoreSuite struct{}
+
+var _ = Suite(&CoreSuite{})
+
+func (s *CoreSuite) TestValidateName(c *C) {
+ validNames := []string{
+ "a", "aa", "aaa", "aaaa",
+ "a-a", "aa-a", "a-aa", "a-b-c",
+ "a0", "a-0", "a-0a",
+ }
+ for _, name := range validNames {
+ err := ValidateName(name)
+ c.Assert(err, IsNil)
+ }
+ invalidNames := []string{
+ // name cannot be empty
+ "",
+ // dashes alone are not a name
+ "-", "--",
+ // double dashes in a name are not allowed
+ "a--a",
+ // name should not end with a dash
+ "a-",
+ // name cannot have any spaces in it
+ "a ", " a", "a a",
+ // a number alone is not a name
+ "0", "123",
+ // identifier must be plain ASCII
+ "日本語", "한글", "ру́сский язы́к",
+ }
+ for _, name := range invalidNames {
+ err := ValidateName(name)
+ c.Assert(err, ErrorMatches, `invalid interface name: ".*"`)
+ }
+}
+
+func (s *CoreSuite) TestValidateDBusBusName(c *C) {
+ // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
+ validNames := []string{
+ "a.b", "a.b.c", "a.b1", "a.b1.c2d",
+ "a_a.b", "a_a.b_b.c_c", "a_a.b_b1", "a_a.b_b1.c_c2d_d",
+ "a-a.b", "a-a.b-b.c-c", "a-a.b-b1", "a-a.b-b1.c-c2d-d",
+ }
+ for _, name := range validNames {
+ err := ValidateDBusBusName(name)
+ c.Assert(err, IsNil)
+ }
+
+ invalidNames := []string{
+ // must not start with ':'
+ ":a.b",
+ // only from [A-Z][a-z][0-9]_-
+ "@.a",
+ // elements may not start with number
+ "0.a",
+ "a.0a",
+ // must have more than one element
+ "a",
+ "a_a",
+ "a-a",
+ // element must not begin with '.'
+ ".a",
+ // each element must be at least 1 character
+ "a.",
+ "a..b",
+ ".a.b",
+ "a.b.",
+ }
+ for _, name := range invalidNames {
+ err := ValidateDBusBusName(name)
+ c.Assert(err, ErrorMatches, `invalid DBus bus name: ".*"`)
+ }
+
+ // must not be empty
+ err := ValidateDBusBusName("")
+ c.Assert(err, ErrorMatches, `DBus bus name must be set`)
+
+ // must not exceed maximum length
+ longName := make([]byte, 256)
+ for i := range longName {
+ longName[i] = 'b'
+ }
+ // make it look otherwise valid (a.bbbb...)
+ longName[0] = 'a'
+ longName[1] = '.'
+ err = ValidateDBusBusName(string(longName))
+ c.Assert(err, ErrorMatches, `DBus bus name is too long \(must be <= 255\)`)
+}
+
+// Plug.Ref works as expected
+func (s *CoreSuite) TestPlugRef(c *C) {
+ plug := &Plug{PlugInfo: &snap.PlugInfo{Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug"}}
+ ref := plug.Ref()
+ c.Check(ref.Snap, Equals, "consumer")
+ c.Check(ref.Name, Equals, "plug")
+}
+
+// Slot.Ref works as expected
+func (s *CoreSuite) TestSlotRef(c *C) {
+ slot := &Slot{SlotInfo: &snap.SlotInfo{Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot"}}
+ ref := slot.Ref()
+ c.Check(ref.Snap, Equals, "producer")
+ c.Check(ref.Name, Equals, "slot")
+}
+
+// ConnRef.ID works as expected
+func (s *CoreSuite) TestConnRefID(c *C) {
+ conn := &ConnRef{
+ PlugRef: PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: SlotRef{Snap: "producer", Name: "slot"},
+ }
+ c.Check(conn.ID(), Equals, "consumer:plug producer:slot")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package dbus implements interaction between snappy and dbus.
+//
+// Snappy creates dbus configuration files that describe how various
+// services on the system bus can communicate with other peers.
+//
+// Each configuration is an XML file containing <busconfig>...</busconfig>.
+// Particular security snippets define whole <policy>...</policy> entires.
+// This is explained in detail in https://dbus.freedesktop.org/doc/dbus-daemon.1.html
+package dbus
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining DBus policy files.
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "dbus"
+}
+
+// Setup creates dbus configuration files specific to a given snap.
+//
+// DBus has no concept of a complain mode so confinment type is ignored.
+func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ // Get the snippets that apply to this snap
+ snippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecurityDBus)
+ if err != nil {
+ return fmt.Errorf("cannot obtain DBus security snippets for snap %q: %s", snapName, err)
+ }
+ // Get the files that this snap should have
+ content, err := b.combineSnippets(snapInfo, snippets)
+ if err != nil {
+ return fmt.Errorf("cannot obtain expected DBus configuration files for snap %q: %s", snapName, err)
+ }
+ glob := fmt.Sprintf("%s.conf", interfaces.SecurityTagGlob(snapName))
+ dir := dirs.SnapBusPolicyDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for DBus configuration files %q: %s", dir, err)
+ }
+ _, _, err = osutil.EnsureDirState(dir, glob, content)
+ if err != nil {
+ return fmt.Errorf("cannot synchronize DBus configuration files for snap %q: %s", snapName, err)
+ }
+ return nil
+}
+
+// Remove removes dbus configuration files of a given snap.
+//
+// This method should be called after removing a snap.
+func (b *Backend) Remove(snapName string) error {
+ glob := fmt.Sprintf("%s.conf", interfaces.SecurityTagGlob(snapName))
+ _, _, err := osutil.EnsureDirState(dirs.SnapBusPolicyDir, glob, nil)
+ if err != nil {
+ return fmt.Errorf("cannot synchronize DBus configuration files for snap %q: %s", snapName, err)
+ }
+ return nil
+}
+
+// combineSnippets combines security snippets collected from all the interfaces
+// affecting a given snap into a content map applicable to EnsureDirState.
+func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) {
+ for _, appInfo := range snapInfo.Apps {
+ securityTag := appInfo.SecurityTag()
+ appSnippets := snippets[securityTag]
+ if len(appSnippets) == 0 {
+ continue
+ }
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+
+ addContent(securityTag, appSnippets, content)
+ }
+
+ for _, hookInfo := range snapInfo.Hooks {
+ securityTag := hookInfo.SecurityTag()
+ hookSnippets := snippets[securityTag]
+ if len(hookSnippets) == 0 {
+ continue
+ }
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+
+ addContent(securityTag, hookSnippets, content)
+ }
+
+ return content, nil
+}
+
+func addContent(securityTag string, executableSnippets [][]byte, content map[string]*osutil.FileState) {
+ var buffer bytes.Buffer
+ buffer.Write(xmlHeader)
+ for _, snippet := range executableSnippets {
+ buffer.Write(snippet)
+ buffer.WriteRune('\n')
+ }
+ buffer.Write(xmlFooter)
+
+ content[fmt.Sprintf("%s.conf", securityTag)] = &osutil.FileState{
+ Content: buffer.Bytes(),
+ Mode: 0644,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package dbus_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/dbus"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+)
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+}
+
+var _ = Suite(&backendSuite{})
+
+var testedConfinementOpts = []interfaces.ConfinementOptions{
+ {},
+ {DevMode: true},
+ {JailMode: true},
+ {Classic: true},
+}
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.Backend = &dbus.Backend{}
+ s.BackendSuite.SetUpTest(c)
+
+ // Prepare a directory for DBus configuration files.
+ // NOTE: Normally this is a part of the OS snap.
+ err := os.MkdirAll(dirs.SnapBusPolicyDir, 0700)
+ c.Assert(err, IsNil)
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.BackendSuite.TearDownTest(c)
+}
+
+// Tests for Setup() and Remove()
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "dbus")
+}
+
+func (s *backendSuite) TestInstallingSnapWritesConfigFiles(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.smbd.conf")
+ // file called "snap.sambda.smbd.conf" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestInstallingSnapWithHookWritesConfigFiles(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.HookYaml, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.foo.hook.configure.conf")
+
+ // Verify that "snap.foo.hook.configure.conf" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesConfigFiles(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.RemoveSnap(c, snapInfo)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.smbd.conf")
+ // file called "snap.sambda.smbd.conf" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapWithHookRemovesConfigFiles(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.HookYaml, 0)
+ s.RemoveSnap(c, snapInfo)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.foo.hook.configure.conf")
+
+ // Verify that "snap.foo.hook.configure.conf" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.nmbd.conf")
+ // file called "snap.sambda.nmbd.conf" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlWithHook, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.hook.configure.conf")
+
+ // Verify that "snap.samba.hook.configure.conf" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1WithNmbd, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.nmbd.conf")
+ // file called "snap.sambda.nmbd.conf" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerHooks(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlWithHook, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.hook.configure.conf")
+
+ // Verify that "snap.samba.hook.configure.conf" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestCombineSnippetsWithActualSnippets(c *C) {
+ // NOTE: replace the real template with a shorter variant
+ restore := dbus.MockXMLEnvelope([]byte("<?xml>\n"), []byte("</xml>"))
+ defer restore()
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy>...</policy>"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.smbd.conf")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ c.Check(string(data), Equals, "<?xml>\n<policy>...</policy>\n</xml>")
+ stat, err := os.Stat(profile)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode(), Equals, os.FileMode(0644))
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestCombineSnippetsWithoutAnySnippets(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.smbd.conf")
+ _, err := os.Stat(profile)
+ // Without any snippets, there the .conf file is not created.
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+const sambaYamlWithIfaceBoundToNmbd = `
+name: samba
+version: 1
+developer: acme
+apps:
+ smbd:
+ nmbd:
+ slots: [iface]
+`
+
+func (s *backendSuite) TestAppBoundIfaces(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("<policy/>"), nil
+ }
+ // Install a snap with two apps, only one of which needs a .conf file
+ // because the interface is app-bound.
+ snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, sambaYamlWithIfaceBoundToNmbd, 0)
+ defer s.RemoveSnap(c, snapInfo)
+ // Check that only one of the .conf files is actually created
+ _, err := os.Stat(filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.smbd.conf"))
+ c.Check(os.IsNotExist(err), Equals, true)
+ _, err = os.Stat(filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.nmbd.conf"))
+ c.Check(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package dbus implements interaction between snappy and dbus.
+//
+// Snappy creates dbus configuration files that describe how various
+// services on the system bus can communicate with other peers.
+//
+// Each configuration is an XML file containing <busconfig>...</busconfig>.
+// Particular security snippets define whole <policy>...</policy> entires.
+//
+// NOTE: This interacts with systemd.
+// TODO: Explain how this works (security).
+package dbus
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+// SafePath returns a string suitable for use in a DBus object
+func SafePath(s string) string {
+ const allowed = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`
+ buf := bytes.NewBuffer(make([]byte, 0, len(s)))
+
+ for _, c := range []byte(s) {
+ if strings.IndexByte(allowed, c) >= 0 {
+ fmt.Fprintf(buf, "%c", c)
+ } else {
+ fmt.Fprintf(buf, "_%02x", c)
+ }
+ }
+
+ return buf.String()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package dbus_test
+
+import (
+ "testing"
+
+ "github.com/snapcore/snapd/interfaces/dbus"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type dBusSuite struct{}
+
+var _ = Suite(&dBusSuite{})
+
+func (s *dBusSuite) TestSecurityGenDbusPath(c *C) {
+ c.Assert(dbus.SafePath("foo"), Equals, "foo")
+ c.Assert(dbus.SafePath("foo bar"), Equals, "foo_20bar")
+ c.Assert(dbus.SafePath("foo/bar"), Equals, "foo_2fbar")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package dbus
+
+// MockXMLEnvelope replaces dbus XML envelope.
+//
+// NOTE: The real XML envelope is not long but is tedious to put into every
+// test. For testing it is convenient for replace it with a shorter version.
+func MockXMLEnvelope(fakeHeader, fakeFooter []byte) (restore func()) {
+ origHeader := xmlHeader
+ origFooter := xmlFooter
+ xmlHeader = fakeHeader
+ xmlFooter = fakeFooter
+ return func() {
+ xmlHeader = origHeader
+ xmlFooter = origFooter
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package dbus
+
+var (
+ xmlHeader = []byte(`<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<!-- This file was automatically generated by snappy -->
+<busconfig>`)
+ xmlFooter = []byte(`</busconfig>`)
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacetest
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type BackendSuite struct {
+ Backend interfaces.SecurityBackend
+ Repo *interfaces.Repository
+ Iface *TestInterface
+ RootDir string
+}
+
+func (s *BackendSuite) SetUpTest(c *C) {
+ // Isolate this test to a temporary directory
+ s.RootDir = c.MkDir()
+ dirs.SetRootDir(s.RootDir)
+ // Create a fresh repository for each test
+ s.Repo = interfaces.NewRepository()
+ s.Iface = &TestInterface{InterfaceName: "iface"}
+ err := s.Repo.AddInterface(s.Iface)
+ c.Assert(err, IsNil)
+}
+
+func (s *BackendSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+// Tests for Setup() and Remove()
+const SambaYamlV1 = `
+name: samba
+version: 1
+developer: acme
+apps:
+ smbd:
+slots:
+ slot:
+ interface: iface
+`
+const SambaYamlV1WithNmbd = `
+name: samba
+version: 1
+developer: acme
+apps:
+ smbd:
+ nmbd:
+slots:
+ slot:
+ interface: iface
+`
+const SambaYamlV1NoSlot = `
+name: samba
+version: 1
+developer: acme
+apps:
+ smbd:
+`
+const SambaYamlV1WithNmbdNoSlot = `
+name: samba
+version: 1
+developer: acme
+apps:
+ smbd:
+ nmbd:
+`
+const SambaYamlV2 = `
+name: samba
+version: 2
+developer: acme
+apps:
+ smbd:
+slots:
+ slot:
+ interface: iface
+`
+const SambaYamlWithHook = `
+name: samba
+apps:
+ smbd:
+ nmbd:
+hooks:
+ configure:
+ plugs: [plug]
+slots:
+ slot:
+ interface: iface
+plugs:
+ plug:
+ interface: iface
+`
+const HookYaml = `
+name: foo
+version: 1
+developer: acme
+hooks:
+ configure:
+plugs:
+ plug:
+ interface: iface
+`
+const PlugNoAppsYaml = `
+name: foo
+version: 1
+developer: acme
+plugs:
+ plug:
+ interface: iface
+`
+const SlotNoAppsYaml = `
+name: foo
+version: 1
+developer: acme
+slots:
+ slots:
+ interface: iface
+`
+
+// Support code for tests
+
+// InstallSnap "installs" a snap from YAML.
+func (s *BackendSuite) InstallSnap(c *C, opts interfaces.ConfinementOptions, snapYaml string, revision int) *snap.Info {
+ snapInfo := snaptest.MockInfo(c, snapYaml, &snap.SideInfo{
+ Revision: snap.R(revision),
+ })
+ s.addPlugsSlots(c, snapInfo)
+ err := s.Backend.Setup(snapInfo, opts, s.Repo)
+ c.Assert(err, IsNil)
+ return snapInfo
+}
+
+// UpdateSnap "updates" an existing snap from YAML.
+func (s *BackendSuite) UpdateSnap(c *C, oldSnapInfo *snap.Info, opts interfaces.ConfinementOptions, snapYaml string, revision int) *snap.Info {
+ newSnapInfo := snaptest.MockInfo(c, snapYaml, &snap.SideInfo{
+ Revision: snap.R(revision),
+ })
+ c.Assert(newSnapInfo.Name(), Equals, oldSnapInfo.Name())
+ s.removePlugsSlots(c, oldSnapInfo)
+ s.addPlugsSlots(c, newSnapInfo)
+ err := s.Backend.Setup(newSnapInfo, opts, s.Repo)
+ c.Assert(err, IsNil)
+ return newSnapInfo
+}
+
+// RemoveSnap "removes" an "installed" snap.
+func (s *BackendSuite) RemoveSnap(c *C, snapInfo *snap.Info) {
+ err := s.Backend.Remove(snapInfo.Name())
+ c.Assert(err, IsNil)
+ s.removePlugsSlots(c, snapInfo)
+}
+
+func (s *BackendSuite) addPlugsSlots(c *C, snapInfo *snap.Info) {
+ for _, plugInfo := range snapInfo.Plugs {
+ plug := &interfaces.Plug{PlugInfo: plugInfo}
+ err := s.Repo.AddPlug(plug)
+ c.Assert(err, IsNil)
+ }
+ for _, slotInfo := range snapInfo.Slots {
+ slot := &interfaces.Slot{SlotInfo: slotInfo}
+ err := s.Repo.AddSlot(slot)
+ c.Assert(err, IsNil)
+ }
+}
+
+func (s *BackendSuite) removePlugsSlots(c *C, snapInfo *snap.Info) {
+ for _, plug := range s.Repo.Plugs(snapInfo.Name()) {
+ err := s.Repo.RemovePlug(plug.Snap.Name(), plug.Name)
+ c.Assert(err, IsNil)
+ }
+ for _, slot := range s.Repo.Slots(snapInfo.Name()) {
+ err := s.Repo.RemoveSlot(slot.Snap.Name(), slot.Name)
+ c.Assert(err, IsNil)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacetest_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacetest
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+// TestSecurityBackend is a security backend intended for testing.
+type TestSecurityBackend struct {
+ // SetupCalls stores information about all calls to Setup
+ SetupCalls []TestSetupCall
+ // RemoveCalls stores information about all calls to Remove
+ RemoveCalls []string
+ // SetupCallback is an callback that is optionally called in Setup
+ SetupCallback func(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error
+ // RemoveCallback is a callback that is optionally called in Remove
+ RemoveCallback func(snapName string) error
+}
+
+// TestSetupCall stores details about calls to TestSecurityBackend.Setup
+type TestSetupCall struct {
+ // SnapInfo is a copy of the snapInfo argument to a particular call to Setup
+ SnapInfo *snap.Info
+ // Options is a copy of the confinement options to a particular call to Setup
+ Options interfaces.ConfinementOptions
+}
+
+// Name returns the name of the security backend.
+func (b *TestSecurityBackend) Name() string {
+ return "test"
+}
+
+// Setup records information about the call and calls the setup callback if one is defined.
+func (b *TestSecurityBackend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ b.SetupCalls = append(b.SetupCalls, TestSetupCall{SnapInfo: snapInfo, Options: opts})
+ if b.SetupCallback == nil {
+ return nil
+ }
+ return b.SetupCallback(snapInfo, opts, repo)
+}
+
+// Remove records information about the call and calls the remove callback if one is defined
+func (b *TestSecurityBackend) Remove(snapName string) error {
+ b.RemoveCalls = append(b.RemoveCalls, snapName)
+ if b.RemoveCallback == nil {
+ return nil
+ }
+ return b.RemoveCallback(snapName)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacetest
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+// TestInterface is a interface for various kind of tests.
+// It is public so that it can be consumed from other packages.
+type TestInterface struct {
+ // InterfaceName is the name of this interface
+ InterfaceName string
+ // AutoConnectCallback is the callback invoked inside AutoConnect
+ AutoConnectCallback func(*interfaces.Plug, *interfaces.Slot) bool
+ // SanitizePlugCallback is the callback invoked inside SanitizePlug()
+ SanitizePlugCallback func(plug *interfaces.Plug) error
+ // SanitizeSlotCallback is the callback invoked inside SanitizeSlot()
+ SanitizeSlotCallback func(slot *interfaces.Slot) error
+ // SlotSnippetCallback is the callback invoked inside ConnectedSlotSnippet()
+ SlotSnippetCallback func(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error)
+ // PermanentSlotSnippetCallback is the callback invoked inside PermanentSlotSnippet()
+ PermanentSlotSnippetCallback func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error)
+ // PlugSnippetCallback is the callback invoked inside ConnectedPlugSnippet()
+ PlugSnippetCallback func(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error)
+ // PermanentPlugSnippetCallback is the callback invoked inside PermanentPlugSnippet()
+ PermanentPlugSnippetCallback func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error)
+}
+
+// String() returns the same value as Name().
+func (t *TestInterface) String() string {
+ return t.Name()
+}
+
+// Name returns the name of the test interface.
+func (t *TestInterface) Name() string {
+ return t.InterfaceName
+}
+
+// SanitizePlug checks and possibly modifies a plug.
+func (t *TestInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if t.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", t))
+ }
+ if t.SanitizePlugCallback != nil {
+ return t.SanitizePlugCallback(plug)
+ }
+ return nil
+}
+
+// SanitizeSlot checks and possibly modifies a slot.
+func (t *TestInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if t.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", t))
+ }
+ if t.SanitizeSlotCallback != nil {
+ return t.SanitizeSlotCallback(slot)
+ }
+ return nil
+}
+
+// ConnectedPlugSnippet returns the configuration snippet "required" to offer a test plug.
+// Providers don't gain any extra permissions.
+func (t *TestInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if t.PlugSnippetCallback != nil {
+ return t.PlugSnippetCallback(plug, slot, securitySystem)
+ }
+ return nil, nil
+}
+
+// PermanentPlugSnippet returns the configuration snippet "required" to offer a test plug.
+// Providers don't gain any extra permissions.
+func (t *TestInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if t.PermanentPlugSnippetCallback != nil {
+ return t.PermanentPlugSnippetCallback(plug, securitySystem)
+ }
+ return nil, nil
+}
+
+// ConnectedSlotSnippet returns the configuration snippet "required" to use a test plug.
+// Consumers don't gain any extra permissions.
+func (t *TestInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if t.SlotSnippetCallback != nil {
+ return t.SlotSnippetCallback(plug, slot, securitySystem)
+ }
+ return nil, nil
+}
+
+// PermanentSlotSnippet returns the configuration snippet "required" to use a test plug.
+// Consumers don't gain any extra permissions.
+func (t *TestInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if t.PermanentSlotSnippetCallback != nil {
+ return t.PermanentSlotSnippetCallback(slot, securitySystem)
+ }
+ return nil, nil
+}
+
+// AutoConnect returns whether plug and slot should be implicitly
+// auto-connected assuming they will be an unambiguous connection
+// candidate.
+func (t *TestInterface) AutoConnect(plug *interfaces.Plug, slot *interfaces.Slot) bool {
+ if t.AutoConnectCallback != nil {
+ return t.AutoConnectCallback(plug, slot)
+ }
+ return true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacetest_test
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/snap"
+)
+
+type TestInterfaceSuite struct {
+ iface interfaces.Interface
+ plug *interfaces.Plug
+ slot *interfaces.Slot
+}
+
+var _ = Suite(&TestInterfaceSuite{
+ iface: &ifacetest.TestInterface{InterfaceName: "test"},
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "snap"},
+ Name: "name",
+ Interface: "test",
+ },
+ },
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "snap"},
+ Name: "name",
+ Interface: "test",
+ },
+ },
+})
+
+// TestInterface has a working Name() function
+func (s *TestInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "test")
+}
+
+// TestInterface doesn't do any sanitization by default
+func (s *TestInterfaceSuite) TestSanitizePlugOK(c *C) {
+ err := s.iface.SanitizePlug(s.plug)
+ c.Assert(err, IsNil)
+}
+
+// TestInterface has provisions to customize sanitization
+func (s *TestInterfaceSuite) TestSanitizePlugError(c *C) {
+ iface := &ifacetest.TestInterface{
+ InterfaceName: "test",
+ SanitizePlugCallback: func(plug *interfaces.Plug) error {
+ return fmt.Errorf("sanitize plug failed")
+ },
+ }
+ err := iface.SanitizePlug(s.plug)
+ c.Assert(err, ErrorMatches, "sanitize plug failed")
+}
+
+// TestInterface sanitization still checks for interface identity
+func (s *TestInterfaceSuite) TestSanitizePlugWrongInterface(c *C) {
+ plug := &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "snap"},
+ Name: "name",
+ Interface: "other-interface",
+ },
+ }
+ c.Assert(func() { s.iface.SanitizePlug(plug) }, Panics, "plug is not of interface \"test\"")
+}
+
+// TestInterface doesn't do any sanitization by default
+func (s *TestInterfaceSuite) TestSanitizeSlotOK(c *C) {
+ err := s.iface.SanitizeSlot(s.slot)
+ c.Assert(err, IsNil)
+}
+
+// TestInterface has provisions to customize sanitization
+func (s *TestInterfaceSuite) TestSanitizeSlotError(c *C) {
+ iface := &ifacetest.TestInterface{
+ InterfaceName: "test",
+ SanitizeSlotCallback: func(slot *interfaces.Slot) error {
+ return fmt.Errorf("sanitize slot failed")
+ },
+ }
+ err := iface.SanitizeSlot(s.slot)
+ c.Assert(err, ErrorMatches, "sanitize slot failed")
+}
+
+// TestInterface sanitization still checks for interface identity
+func (s *TestInterfaceSuite) TestSanitizeSlotWrongInterface(c *C) {
+ slot := &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "snap"},
+ Name: "name",
+ Interface: "interface",
+ },
+ }
+ c.Assert(func() { s.iface.SanitizeSlot(slot) }, Panics, "slot is not of interface \"test\"")
+}
+
+// TestInterface hands out empty plug security snippets
+func (s *TestInterfaceSuite) TestPlugSnippet(c *C) {
+ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, "foo")
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+// TestInterface hands out empty slot security snippets
+func (s *TestInterfaceSuite) TestSlotSnippet(c *C) {
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecuritySecComp)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityDBus)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+ snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, "foo")
+ c.Assert(err, IsNil)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *TestInterfaceSuite) TestAutoConnect(c *C) {
+ c.Check(s.iface.AutoConnect(nil, nil), Equals, true)
+
+ iface := &ifacetest.TestInterface{AutoConnectCallback: func(*interfaces.Plug, *interfaces.Slot) bool { return false }}
+
+ c.Check(iface.AutoConnect(nil, nil), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+import (
+ "encoding/json"
+)
+
+// plugJSON aids in marshaling Plug into JSON.
+type plugJSON struct {
+ Snap string `json:"snap"`
+ Name string `json:"plug"`
+ Interface string `json:"interface"`
+ Attrs map[string]interface{} `json:"attrs,omitempty"`
+ Apps []string `json:"apps,omitempty"`
+ Label string `json:"label"`
+ Connections []SlotRef `json:"connections,omitempty"`
+}
+
+// MarshalJSON returns the JSON encoding of plug.
+func (plug *Plug) MarshalJSON() ([]byte, error) {
+ var names []string
+ for name := range plug.Apps {
+ names = append(names, name)
+ }
+ return json.Marshal(&plugJSON{
+ Snap: plug.Snap.Name(),
+ Name: plug.Name,
+ Interface: plug.Interface,
+ Attrs: plug.Attrs,
+ Apps: names,
+ Label: plug.Label,
+ Connections: plug.Connections,
+ })
+}
+
+// slotJSON aids in marshaling Slot into JSON.
+type slotJSON struct {
+ Snap string `json:"snap"`
+ Name string `json:"slot"`
+ Interface string `json:"interface"`
+ Attrs map[string]interface{} `json:"attrs,omitempty"`
+ Apps []string `json:"apps,omitempty"`
+ Label string `json:"label"`
+ Connections []PlugRef `json:"connections,omitempty"`
+}
+
+// MarshalJSON returns the JSON encoding of slot.
+func (slot *Slot) MarshalJSON() ([]byte, error) {
+ var names []string
+ for name := range slot.Apps {
+ names = append(names, name)
+ }
+ return json.Marshal(&slotJSON{
+ Snap: slot.Snap.Name(),
+ Name: slot.Name,
+ Interface: slot.Interface,
+ Attrs: slot.Attrs,
+ Apps: names,
+ Label: slot.Label,
+ Connections: slot.Connections,
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces_test
+
+import (
+ "encoding/json"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+type JSONSuite struct{}
+
+var _ = Suite(&JSONSuite{})
+
+func (s *JSONSuite) TestPlugMarshalJSON(c *C) {
+ plug := &Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "snap-name"},
+ Name: "plug-name",
+ Interface: "interface",
+ Attrs: map[string]interface{}{"key": "value"},
+ Apps: map[string]*snap.AppInfo{
+ "app-name": {
+ Name: "app-name",
+ },
+ },
+ Label: "label",
+ },
+ Connections: []SlotRef{{
+ Snap: "other-snap-name",
+ Name: "slot-name",
+ }},
+ }
+ data, err := json.Marshal(plug)
+ c.Assert(err, IsNil)
+ var repr map[string]interface{}
+ err = json.Unmarshal(data, &repr)
+ c.Assert(err, IsNil)
+ c.Check(repr, DeepEquals, map[string]interface{}{
+ "snap": "snap-name",
+ "plug": "plug-name",
+ "interface": "interface",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app-name"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "other-snap-name", "slot": "slot-name"},
+ },
+ })
+}
+
+func (s *JSONSuite) TestSlotMarshalJSON(c *C) {
+ slot := &Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "snap-name"},
+ Name: "slot-name",
+ Interface: "interface",
+ Attrs: map[string]interface{}{"key": "value"},
+ Apps: map[string]*snap.AppInfo{
+ "app-name": {
+ Name: "app-name",
+ },
+ },
+ Label: "label",
+ },
+ Connections: []PlugRef{{
+ Snap: "other-snap-name",
+ Name: "plug-name",
+ }},
+ }
+ data, err := json.Marshal(slot)
+ c.Assert(err, IsNil)
+ var repr map[string]interface{}
+ err = json.Unmarshal(data, &repr)
+ c.Assert(err, IsNil)
+ c.Check(repr, DeepEquals, map[string]interface{}{
+ "snap": "snap-name",
+ "slot": "slot-name",
+ "interface": "interface",
+ "attrs": map[string]interface{}{"key": "value"},
+ "apps": []interface{}{"app-name"},
+ "label": "label",
+ "connections": []interface{}{
+ map[string]interface{}{"snap": "other-snap-name", "plug": "plug-name"},
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package kmod implements a backend which loads kernel modules on behalf of
+// interfaces.
+//
+// Interfaces may request kernel modules to be loaded by providing snippets via
+// their respective "*Snippet" methods for interfaces.SecurityKMod security
+// system. The snippet should contain a newline-separated list of requested
+// kernel modules. The KMod backend stores all the modules needed by given
+// snap in /etc/modules-load.d/snap.<snapname>.conf file ensuring they are
+// loaded when the system boots and also loads these modules via modprobe.
+// If a snap is uninstalled or respective interface gets disconnected, the
+// corresponding /etc/modules-load.d/ config file gets removed, however no
+// kernel modules are unloaded. This is by design.
+//
+// Note: this mechanism should not be confused with kernel-module-interface;
+// kmod only loads a well-defined list of modules provided by interface definition
+// and doesn't grant any special permissions related to kernel modules to snaps,
+// in contrast to kernel-module-interface.
+package kmod
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "sort"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining kernel modules
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "kmod"
+}
+
+// Setup creates a conf file with list of kernel modules required by given snap,
+// writes it in /etc/modules-load.d/ directory and immediately loads the modules
+// using /sbin/modprobe. The devMode is ignored.
+//
+// If the method fails it should be re-tried (with a sensible strategy) by the caller.
+func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ // Get the snippets that apply to this snap
+ snippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecurityKMod)
+ if err != nil {
+ return fmt.Errorf("cannot obtain kmod security snippets for snap %q: %s", snapName, err)
+ }
+
+ // Get the files that this snap should have
+ glob := interfaces.SecurityTagGlob(snapName)
+ content, modules, err := b.combineSnippets(snapInfo, snippets)
+ if err != nil {
+ return fmt.Errorf("cannot obtain expected security files for snap %q: %s", snapName, err)
+ }
+
+ dir := dirs.SnapKModModulesDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err)
+ }
+
+ changed, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, content)
+ if err != nil {
+ return err
+ }
+
+ if len(changed) > 0 {
+ return loadModules(modules)
+ }
+ return nil
+}
+
+// Remove removes modules config file specific to a given snap.
+//
+// This method should be called after removing a snap.
+//
+// If the method fails it should be re-tried (with a sensible strategy) by the caller.
+func (b *Backend) Remove(snapName string) error {
+ glob := interfaces.SecurityTagGlob(snapName)
+ _, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, nil)
+ return err
+}
+
+// combineSnippets combines security snippets collected from all the interfaces
+// affecting a given snap into a de-duplicated list of kernel modules.
+func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, modules []string, err error) {
+ content = make(map[string]*osutil.FileState)
+
+ for _, appInfo := range snapInfo.Apps {
+ for _, snippet := range snippets[appInfo.SecurityTag()] {
+ // split snippet by newline to get the list of modules
+ for _, line := range bytes.Split(snippet, []byte{'\n'}) {
+ l := bytes.TrimSpace(line)
+ // ignore empty lines and comments
+ if len(l) > 0 && l[0] != '#' {
+ modules = append(modules, string(l))
+ }
+ }
+ }
+ }
+
+ sort.Strings(modules)
+ modules = uniqueLines(modules)
+ if len(modules) > 0 {
+ var buffer bytes.Buffer
+ buffer.WriteString("# This file is automatically generated.\n")
+ for _, module := range modules {
+ buffer.WriteString(module)
+ buffer.WriteByte('\n')
+ }
+
+ content[fmt.Sprintf("%s.conf", snap.SecurityTag(snapInfo.Name()))] = &osutil.FileState{
+ Content: buffer.Bytes(),
+ Mode: 0644,
+ }
+ }
+
+ return content, modules, nil
+}
+
+func uniqueLines(lines []string) (deduplicated []string) {
+ dedup := make(map[string]bool)
+ for _, line := range lines {
+ if !dedup[line] {
+ dedup[line] = true
+ deduplicated = append(deduplicated, line)
+ }
+ }
+ return deduplicated
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package kmod_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ "github.com/snapcore/snapd/testutil"
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/interfaces/kmod"
+ "github.com/snapcore/snapd/osutil"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+ modprobeCmd *testutil.MockCmd
+}
+
+var _ = Suite(&backendSuite{})
+
+var testedConfinementOpts = []interfaces.ConfinementOptions{
+ {},
+ {DevMode: true},
+ {JailMode: true},
+ {Classic: true},
+}
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.Backend = &kmod.Backend{}
+ s.BackendSuite.SetUpTest(c)
+ s.modprobeCmd = testutil.MockCommand(c, "modprobe", "")
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.modprobeCmd.Restore()
+ s.BackendSuite.TearDownTest(c)
+}
+
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "kmod")
+}
+
+func (s *backendSuite) TestUniqueLines(c *C) {
+ data := []string{
+ "module1",
+ "module2",
+ "module3",
+ "module2",
+ }
+ out := kmod.UniqueLines(data)
+ c.Assert(out, HasLen, 3)
+
+ c.Assert(out[0], Equals, "module1")
+ c.Assert(out[1], Equals, "module2")
+ c.Assert(out[2], Equals, "module3")
+
+ data = []string{}
+ out = kmod.UniqueLines(data)
+ c.Assert(out, HasLen, 0)
+}
+
+func (s *backendSuite) TestInstallingSnapCreatesModulesConf(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if securitySystem == interfaces.SecurityKMod {
+ return []byte("module1 \n module2\nmodule1\n#\n"), nil
+ }
+ return nil, nil
+ }
+
+ path := filepath.Join(dirs.SnapKModModulesDir, "snap.samba.conf")
+ c.Assert(osutil.FileExists(path), Equals, false)
+
+ for _, opts := range testedConfinementOpts {
+ s.modprobeCmd.ForgetCalls()
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+
+ c.Assert(osutil.FileExists(path), Equals, true)
+ modfile, err := ioutil.ReadFile(path)
+ c.Assert(err, IsNil)
+ c.Assert(string(modfile), Equals, "# This file is automatically generated.\nmodule1\nmodule2\n")
+
+ c.Assert(s.modprobeCmd.Calls(), DeepEquals, [][]string{
+ {"modprobe", "--syslog", "module1"},
+ {"modprobe", "--syslog", "module2"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesModulesConf(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if securitySystem == interfaces.SecurityKMod {
+ return []byte("module1\nmodule2"), nil
+ }
+ return nil, nil
+ }
+
+ path := filepath.Join(dirs.SnapKModModulesDir, "snap.samba.conf")
+ c.Assert(osutil.FileExists(path), Equals, false)
+
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ c.Assert(osutil.FileExists(path), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ c.Assert(osutil.FileExists(path), Equals, false)
+ }
+}
+
+func (s *backendSuite) TestSecurityIsStable(c *C) {
+ // NOTE: Hand out a permanent snippet so that .conf file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if securitySystem == interfaces.SecurityKMod {
+ return []byte("module1\nmodule2"), nil
+ }
+ return nil, nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.modprobeCmd.ForgetCalls()
+ err := s.Backend.Setup(snapInfo, opts, s.Repo)
+ c.Assert(err, IsNil)
+ // modules conf is not re-loaded when nothing changes
+ c.Check(s.modprobeCmd.Calls(), HasLen, 0)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package kmod
+
+var (
+ LoadModules = loadModules
+ UniqueLines = uniqueLines
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package kmod
+
+import (
+ "fmt"
+ "os/exec"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+func LoadModule(module string) error {
+ if output, err := exec.Command("modprobe", "--syslog", module).CombinedOutput(); err != nil {
+ return fmt.Errorf("cannot load module %s: %s", module, osutil.OutputErr(output, err))
+ }
+ return nil
+}
+
+// loadModules loads given list of modules via modprobe.
+// Any error from modprobe interrupts loading of subsequent modules and returns the error.
+func loadModules(modules []string) error {
+ for _, mod := range modules {
+ if err := LoadModule(mod); err != nil {
+ return err
+ }
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package kmod_test
+
+import (
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/interfaces/kmod"
+ "github.com/snapcore/snapd/testutil"
+ . "gopkg.in/check.v1"
+)
+
+type kmodSuite struct {
+ ifacetest.BackendSuite
+}
+
+var _ = Suite(&kmodSuite{})
+
+func (s *kmodSuite) SetUpTest(c *C) {
+ s.Backend = &kmod.Backend{}
+ s.BackendSuite.SetUpTest(c)
+}
+
+func (s *kmodSuite) TearDownTest(c *C) {
+ s.BackendSuite.TearDownTest(c)
+}
+
+func (s *kmodSuite) TestModprobeCall(c *C) {
+ cmd := testutil.MockCommand(c, "modprobe", "")
+ defer cmd.Restore()
+
+ err := kmod.LoadModules([]string{
+ "module1",
+ "module2",
+ })
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"modprobe", "--syslog", "module1"},
+ {"modprobe", "--syslog", "module2"},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package mount implements mounts that get mapped into the snap
+//
+// Snappy creates fstab like configuration files that describe what
+// directories from the system or from other snaps should get mapped
+// into the snap.
+//
+// Each fstab like file looks like a regular fstab entry:
+// /src/dir /dst/dir none bind 0 0
+// /src/dir /dst/dir none bind,rw 0 0
+// but only bind mounts are supported
+package mount
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining mount files for snap-confine
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "mount"
+}
+
+// Setup creates mount mount profile files specific to a given snap.
+func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ // Get the snippets that apply to this snap
+ snippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecurityMount)
+ if err != nil {
+ return fmt.Errorf("cannot obtain mount security snippets for snap %q: %s", snapName, err)
+ }
+ // Get the files that this snap should have
+ content, err := b.combineSnippets(snapInfo, snippets)
+ if err != nil {
+ return fmt.Errorf("cannot obtain expected mount configuration files for snap %q: %s", snapName, err)
+ }
+ glob := fmt.Sprintf("%s.fstab", interfaces.SecurityTagGlob(snapName))
+ dir := dirs.SnapMountPolicyDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for mount configuration files %q: %s", dir, err)
+ }
+ _, _, err = osutil.EnsureDirState(dir, glob, content)
+ if err != nil {
+ return fmt.Errorf("cannot synchronize mount configuration files for snap %q: %s", snapName, err)
+ }
+ return nil
+}
+
+// Remove removes mount configuration files of a given snap.
+//
+// This method should be called after removing a snap.
+func (b *Backend) Remove(snapName string) error {
+ glob := fmt.Sprintf("%s.fstab", interfaces.SecurityTagGlob(snapName))
+ _, _, err := osutil.EnsureDirState(dirs.SnapMountPolicyDir, glob, nil)
+ if err != nil {
+ return fmt.Errorf("cannot synchronize mount configuration files for snap %q: %s", snapName, err)
+ }
+ return nil
+}
+
+// combineSnippets combines security snippets collected from all the interfaces
+// affecting a given snap into a content map applicable to EnsureDirState.
+func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) {
+ for _, appInfo := range snapInfo.Apps {
+ securityTag := appInfo.SecurityTag()
+ appSnippets := snippets[securityTag]
+ if len(appSnippets) == 0 {
+ continue
+ }
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+
+ addContent(securityTag, appSnippets, content)
+ }
+
+ for _, hookInfo := range snapInfo.Hooks {
+ securityTag := hookInfo.SecurityTag()
+ hookSnippets := snippets[securityTag]
+ if len(hookSnippets) == 0 {
+ continue
+ }
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+
+ addContent(securityTag, hookSnippets, content)
+ }
+ return content, nil
+}
+
+func addContent(securityTag string, executableSnippets [][]byte, content map[string]*osutil.FileState) {
+ var buffer bytes.Buffer
+ for _, snippet := range executableSnippets {
+ buffer.Write(snippet)
+ buffer.WriteRune('\n')
+ }
+
+ content[fmt.Sprintf("%s.fstab", securityTag)] = &osutil.FileState{
+ Content: buffer.Bytes(),
+ Mode: 0644,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package mount_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/interfaces/mount"
+ "github.com/snapcore/snapd/osutil"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+
+ iface2 *ifacetest.TestInterface
+}
+
+var _ = Suite(&backendSuite{})
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.Backend = &mount.Backend{}
+ s.BackendSuite.SetUpTest(c)
+
+ err := os.MkdirAll(dirs.SnapMountPolicyDir, 0700)
+ c.Assert(err, IsNil)
+
+ // add second iface so that we actually test combining snippets
+ s.iface2 = &ifacetest.TestInterface{InterfaceName: "iface2"}
+ err = s.Repo.AddInterface(s.iface2)
+ c.Assert(err, IsNil)
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.BackendSuite.TearDownTest(c)
+}
+
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "mount")
+}
+
+func (s *backendSuite) TestRemove(c *C) {
+ appCanaryToGo := filepath.Join(dirs.SnapMountPolicyDir, "snap.hello-world.hello-world.fstab")
+ err := ioutil.WriteFile(appCanaryToGo, []byte("ni! ni! ni!"), 0644)
+ c.Assert(err, IsNil)
+
+ hookCanaryToGo := filepath.Join(dirs.SnapMountPolicyDir, "snap.hello-world.hook.configure.fstab")
+ err = ioutil.WriteFile(hookCanaryToGo, []byte("ni! ni! ni!"), 0644)
+ c.Assert(err, IsNil)
+
+ canaryToStay := filepath.Join(dirs.SnapMountPolicyDir, "snap.i-stay.really.fstab")
+ err = ioutil.WriteFile(canaryToStay, []byte("stay!"), 0644)
+ c.Assert(err, IsNil)
+
+ err = s.Backend.Remove("hello-world")
+ c.Assert(err, IsNil)
+
+ c.Assert(osutil.FileExists(appCanaryToGo), Equals, false)
+ c.Assert(osutil.FileExists(hookCanaryToGo), Equals, false)
+ content, err := ioutil.ReadFile(canaryToStay)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "stay!")
+}
+
+var mockSnapYaml = `name: snap-name
+version: 1
+apps:
+ app1:
+ app2:
+hooks:
+ configure:
+ plugs: [iface-plug, iface2-plug]
+plugs:
+ iface-plug:
+ interface: iface
+ iface2-plug:
+ interface: iface2
+slots:
+ iface-slot:
+ interface: iface
+ iface2-slot:
+ interface: iface2
+`
+
+func (s *backendSuite) TestSetupSetsupSimple(c *C) {
+ fsEntryIF1 := "/src-1 /dst-1 none bind,ro 0 0"
+ fsEntryIF2 := "/src-2 /dst-2 none bind,ro 0 0"
+
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(fsEntryIF1), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(fsEntryIF1), nil
+ }
+ s.iface2.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(fsEntryIF2), nil
+ }
+ s.iface2.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(fsEntryIF2), nil
+ }
+
+ // confinement options are irrelevant to this security backend
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, mockSnapYaml, 0)
+
+ // ensure both security snippets for iface/iface2 are combined
+ expected := strings.Split(fmt.Sprintf("%s\n%s\n", fsEntryIF1, fsEntryIF2), "\n")
+ sort.Strings(expected)
+ // and we have them both for both apps and the hook
+ for _, binary := range []string{"app1", "app2", "hook.configure"} {
+ fn1 := filepath.Join(dirs.SnapMountPolicyDir, fmt.Sprintf("snap.snap-name.%s.fstab", binary))
+ content, err := ioutil.ReadFile(fn1)
+ c.Assert(err, IsNil, Commentf("Expected mount file for %q", binary))
+ got := strings.Split(string(content), "\n")
+ sort.Strings(got)
+ c.Check(got, DeepEquals, expected)
+ }
+}
+
+func (s *backendSuite) TestSetupSetsupWithoutDir(c *C) {
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("xxx"), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("xxx"), nil
+ }
+
+ // Ensure that backend.Setup() creates the required dir on demand
+ os.Remove(dirs.SnapMountPolicyDir)
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, mockSnapYaml, 0)
+
+ for _, binary := range []string{"app1", "app2", "hook.configure"} {
+ fn := filepath.Join(dirs.SnapMountPolicyDir, fmt.Sprintf("snap.snap-name.%s.fstab", binary))
+ c.Assert(osutil.FileExists(fn), Equals, true, Commentf("Expected mount file for %q", binary))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+import (
+ "github.com/snapcore/snapd/snap"
+)
+
+// SecurityTagGlob returns a pattern that matches all security tags belonging to
+// the same snap as the given app.
+func SecurityTagGlob(snapName string) string {
+ return snap.AppSecurityTag(snapName, "*")
+}
+
+func InterfaceServiceName(snapName, uniqueName string) string {
+ return snap.ScopedSecurityTag(snapName, "interface", uniqueName) + ".service"
+}
--- /dev/null
+// -*- Mote: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/interfaces"
+)
+
+type NamingSuite struct{}
+
+var _ = Suite(&NamingSuite{})
+
+func (s *NamingSuite) TestSecurityTagGlob(c *C) {
+ c.Check(SecurityTagGlob("http"), Equals, "snap.http.*")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package policy
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+// check helpers
+
+func checkSnapType(snapType snap.Type, types []string) error {
+ if len(types) == 0 {
+ return nil
+ }
+ s := string(snapType)
+ if s == "os" { // we use "core" in the assertions
+ s = "core"
+ }
+ for _, t := range types {
+ if t == s {
+ return nil
+ }
+ }
+ return fmt.Errorf("snap type does not match")
+}
+
+func checkID(kind, id string, ids []string, special map[string]string) error {
+ if len(ids) == 0 {
+ return nil
+ }
+ if id == "" { // unset values never match
+ return fmt.Errorf("%s does not match", kind)
+ }
+ for _, cand := range ids {
+ if strings.HasPrefix(cand, "$") {
+ cand = special[cand]
+ if cand == "" { // we ignore unknown special "ids"
+ continue
+ }
+ }
+ if id == cand {
+ return nil
+ }
+ }
+ return fmt.Errorf("%s does not match", kind)
+}
+
+func checkOnClassic(c *asserts.OnClassicConstraint) error {
+ if c == nil {
+ return nil
+ }
+ if c.Classic != release.OnClassic {
+ return fmt.Errorf("on-classic mismatch")
+ }
+ if c.Classic && len(c.SystemIDs) != 0 {
+ return checkID("operating system ID", release.ReleaseInfo.ID, c.SystemIDs, nil)
+ }
+ return nil
+}
+
+func checkPlugConnectionConstraints1(connc *ConnectCandidate, cstrs *asserts.PlugConnectionConstraints) error {
+ if err := cstrs.PlugAttributes.Check(connc.plugAttrs()); err != nil {
+ return err
+ }
+ if err := cstrs.SlotAttributes.Check(connc.slotAttrs()); err != nil {
+ return err
+ }
+ if err := checkSnapType(connc.slotSnapType(), cstrs.SlotSnapTypes); err != nil {
+ return err
+ }
+ if err := checkID("snap id", connc.slotSnapID(), cstrs.SlotSnapIDs, nil); err != nil {
+ return err
+ }
+ err := checkID("publisher id", connc.slotPublisherID(), cstrs.SlotPublisherIDs, map[string]string{
+ "$PLUG_PUBLISHER_ID": connc.plugPublisherID(),
+ })
+ if err != nil {
+ return err
+ }
+ if err := checkOnClassic(cstrs.OnClassic); err != nil {
+ return err
+ }
+ return nil
+}
+
+func checkPlugConnectionConstraints(connc *ConnectCandidate, cstrs []*asserts.PlugConnectionConstraints) error {
+ var firstErr error
+ // OR of constraints
+ for _, cstrs1 := range cstrs {
+ err := checkPlugConnectionConstraints1(connc, cstrs1)
+ if err == nil {
+ return nil
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
+
+func checkSlotConnectionConstraints1(connc *ConnectCandidate, cstrs *asserts.SlotConnectionConstraints) error {
+ if err := cstrs.PlugAttributes.Check(connc.plugAttrs()); err != nil {
+ return err
+ }
+ if err := cstrs.SlotAttributes.Check(connc.slotAttrs()); err != nil {
+ return err
+ }
+ if err := checkSnapType(connc.plugSnapType(), cstrs.PlugSnapTypes); err != nil {
+ return err
+ }
+ if err := checkID("snap id", connc.plugSnapID(), cstrs.PlugSnapIDs, nil); err != nil {
+ return err
+ }
+ err := checkID("publisher id", connc.plugPublisherID(), cstrs.PlugPublisherIDs, map[string]string{
+ "$SLOT_PUBLISHER_ID": connc.slotPublisherID(),
+ })
+ if err != nil {
+ return err
+ }
+ if err := checkOnClassic(cstrs.OnClassic); err != nil {
+ return err
+ }
+ return nil
+}
+
+func checkSlotConnectionConstraints(connc *ConnectCandidate, cstrs []*asserts.SlotConnectionConstraints) error {
+ var firstErr error
+ // OR of constraints
+ for _, cstrs1 := range cstrs {
+ err := checkSlotConnectionConstraints1(connc, cstrs1)
+ if err == nil {
+ return nil
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
+
+func checkSlotInstallationConstraints1(slot *snap.SlotInfo, cstrs *asserts.SlotInstallationConstraints) error {
+ if err := cstrs.SlotAttributes.Check(slot.Attrs); err != nil {
+ return err
+ }
+ if err := checkSnapType(slot.Snap.Type, cstrs.SlotSnapTypes); err != nil {
+ return err
+ }
+ if err := checkOnClassic(cstrs.OnClassic); err != nil {
+ return err
+ }
+ return nil
+}
+
+func checkSlotInstallationConstraints(slot *snap.SlotInfo, cstrs []*asserts.SlotInstallationConstraints) error {
+ var firstErr error
+ // OR of constraints
+ for _, cstrs1 := range cstrs {
+ err := checkSlotInstallationConstraints1(slot, cstrs1)
+ if err == nil {
+ return nil
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
+
+func checkPlugInstallationConstraints1(plug *snap.PlugInfo, cstrs *asserts.PlugInstallationConstraints) error {
+ if err := cstrs.PlugAttributes.Check(plug.Attrs); err != nil {
+ return err
+ }
+ if err := checkSnapType(plug.Snap.Type, cstrs.PlugSnapTypes); err != nil {
+ return err
+ }
+ if err := checkOnClassic(cstrs.OnClassic); err != nil {
+ return err
+ }
+ return nil
+}
+
+func checkPlugInstallationConstraints(plug *snap.PlugInfo, cstrs []*asserts.PlugInstallationConstraints) error {
+ var firstErr error
+ // OR of constraints
+ for _, cstrs1 := range cstrs {
+ err := checkPlugInstallationConstraints1(plug, cstrs1)
+ if err == nil {
+ return nil
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package policy implements the declaration based policy checks for
+// connecting or permitting installation of snaps based on their slots
+// and plugs.
+package policy
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/snap"
+)
+
+// InstallCandidate represents a candidate snap for installation.
+type InstallCandidate struct {
+ Snap *snap.Info
+ SnapDeclaration *asserts.SnapDeclaration
+ BaseDeclaration *asserts.BaseDeclaration
+}
+
+func (ic *InstallCandidate) checkSlotRule(slot *snap.SlotInfo, rule *asserts.SlotRule, snapRule bool) error {
+ context := ""
+ if snapRule {
+ context = fmt.Sprintf(" for %q snap", ic.SnapDeclaration.SnapName())
+ }
+ if checkSlotInstallationConstraints(slot, rule.DenyInstallation) == nil {
+ return fmt.Errorf("installation denied by %q slot rule of interface %q%s", slot.Name, slot.Interface, context)
+ }
+ if checkSlotInstallationConstraints(slot, rule.AllowInstallation) != nil {
+ return fmt.Errorf("installation not allowed by %q slot rule of interface %q%s", slot.Name, slot.Interface, context)
+ }
+ return nil
+}
+
+func (ic *InstallCandidate) checkPlugRule(plug *snap.PlugInfo, rule *asserts.PlugRule, snapRule bool) error {
+ context := ""
+ if snapRule {
+ context = fmt.Sprintf(" for %q snap", ic.SnapDeclaration.SnapName())
+ }
+ if checkPlugInstallationConstraints(plug, rule.DenyInstallation) == nil {
+ return fmt.Errorf("installation denied by %q plug rule of interface %q%s", plug.Name, plug.Interface, context)
+ }
+ if checkPlugInstallationConstraints(plug, rule.AllowInstallation) != nil {
+ return fmt.Errorf("installation not allowed by %q plug rule of interface %q%s", plug.Name, plug.Interface, context)
+ }
+ return nil
+}
+
+func (ic *InstallCandidate) checkSlot(slot *snap.SlotInfo) error {
+ iface := slot.Interface
+ if snapDecl := ic.SnapDeclaration; snapDecl != nil {
+ if rule := snapDecl.SlotRule(iface); rule != nil {
+ return ic.checkSlotRule(slot, rule, true)
+ }
+ }
+ if rule := ic.BaseDeclaration.SlotRule(iface); rule != nil {
+ return ic.checkSlotRule(slot, rule, false)
+ }
+ return nil
+}
+
+func (ic *InstallCandidate) checkPlug(plug *snap.PlugInfo) error {
+ iface := plug.Interface
+ if snapDecl := ic.SnapDeclaration; snapDecl != nil {
+ if rule := snapDecl.PlugRule(iface); rule != nil {
+ return ic.checkPlugRule(plug, rule, true)
+ }
+ }
+ if rule := ic.BaseDeclaration.PlugRule(iface); rule != nil {
+ return ic.checkPlugRule(plug, rule, false)
+ }
+ return nil
+}
+
+// Check checks whether the installation is allowed.
+func (ic *InstallCandidate) Check() error {
+ if ic.BaseDeclaration == nil {
+ return fmt.Errorf("internal error: improperly initialized InstallCandidate")
+ }
+
+ for _, slot := range ic.Snap.Slots {
+ err := ic.checkSlot(slot)
+ if err != nil {
+ return err
+ }
+ }
+
+ for _, plug := range ic.Snap.Plugs {
+ err := ic.checkPlug(plug)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// ConnectCandidate represents a candidate connection.
+type ConnectCandidate struct {
+ // TODO: later we need to carry dynamic attributes once we have those
+ Plug *snap.PlugInfo
+ PlugSnapDeclaration *asserts.SnapDeclaration
+
+ Slot *snap.SlotInfo
+ SlotSnapDeclaration *asserts.SnapDeclaration
+
+ BaseDeclaration *asserts.BaseDeclaration
+}
+
+func (connc *ConnectCandidate) plugAttrs() map[string]interface{} {
+ return connc.Plug.Attrs
+}
+
+func (connc *ConnectCandidate) slotAttrs() map[string]interface{} {
+ return connc.Slot.Attrs
+}
+
+func (connc *ConnectCandidate) plugSnapType() snap.Type {
+ return connc.Plug.Snap.Type
+}
+
+func (connc *ConnectCandidate) slotSnapType() snap.Type {
+ return connc.Slot.Snap.Type
+}
+
+func (connc *ConnectCandidate) plugSnapID() string {
+ if connc.PlugSnapDeclaration != nil {
+ return connc.PlugSnapDeclaration.SnapID()
+ }
+ return "" // never a valid snap-id
+}
+
+func (connc *ConnectCandidate) slotSnapID() string {
+ if connc.SlotSnapDeclaration != nil {
+ return connc.SlotSnapDeclaration.SnapID()
+ }
+ return "" // never a valid snap-id
+}
+
+func (connc *ConnectCandidate) plugPublisherID() string {
+ if connc.PlugSnapDeclaration != nil {
+ return connc.PlugSnapDeclaration.PublisherID()
+ }
+ return "" // never a valid publisher-id
+}
+
+func (connc *ConnectCandidate) slotPublisherID() string {
+ if connc.SlotSnapDeclaration != nil {
+ return connc.SlotSnapDeclaration.PublisherID()
+ }
+ return "" // never a valid publisher-id
+}
+
+func (connc *ConnectCandidate) checkPlugRule(kind string, rule *asserts.PlugRule, snapRule bool) error {
+ context := ""
+ if snapRule {
+ context = fmt.Sprintf(" for %q snap", connc.PlugSnapDeclaration.SnapName())
+ }
+ denyConst := rule.DenyConnection
+ allowConst := rule.AllowConnection
+ if kind == "auto-connection" {
+ denyConst = rule.DenyAutoConnection
+ allowConst = rule.AllowAutoConnection
+ }
+ if checkPlugConnectionConstraints(connc, denyConst) == nil {
+ return fmt.Errorf("%s denied by plug rule of interface %q%s", kind, connc.Plug.Interface, context)
+ }
+ if checkPlugConnectionConstraints(connc, allowConst) != nil {
+ return fmt.Errorf("%s not allowed by plug rule of interface %q%s", kind, connc.Plug.Interface, context)
+ }
+ return nil
+}
+
+func (connc *ConnectCandidate) checkSlotRule(kind string, rule *asserts.SlotRule, snapRule bool) error {
+ context := ""
+ if snapRule {
+ context = fmt.Sprintf(" for %q snap", connc.SlotSnapDeclaration.SnapName())
+ }
+ denyConst := rule.DenyConnection
+ allowConst := rule.AllowConnection
+ if kind == "auto-connection" {
+ denyConst = rule.DenyAutoConnection
+ allowConst = rule.AllowAutoConnection
+ }
+ if checkSlotConnectionConstraints(connc, denyConst) == nil {
+ return fmt.Errorf("%s denied by slot rule of interface %q%s", kind, connc.Plug.Interface, context)
+ }
+ if checkSlotConnectionConstraints(connc, allowConst) != nil {
+ return fmt.Errorf("%s not allowed by slot rule of interface %q%s", kind, connc.Plug.Interface, context)
+ }
+ return nil
+}
+
+func (connc *ConnectCandidate) check(kind string) error {
+ baseDecl := connc.BaseDeclaration
+ if baseDecl == nil {
+ return fmt.Errorf("internal error: improperly initialized ConnectCandidate")
+ }
+
+ iface := connc.Plug.Interface
+
+ if connc.Slot.Interface != iface {
+ return fmt.Errorf("cannot connect mismatched plug interface %q to slot interface %q", iface, connc.Slot.Interface)
+ }
+
+ if plugDecl := connc.PlugSnapDeclaration; plugDecl != nil {
+ if rule := plugDecl.PlugRule(iface); rule != nil {
+ return connc.checkPlugRule(kind, rule, true)
+ }
+ }
+ if slotDecl := connc.SlotSnapDeclaration; slotDecl != nil {
+ if rule := slotDecl.SlotRule(iface); rule != nil {
+ return connc.checkSlotRule(kind, rule, true)
+ }
+ }
+ if rule := baseDecl.PlugRule(iface); rule != nil {
+ return connc.checkPlugRule(kind, rule, false)
+ }
+ if rule := baseDecl.SlotRule(iface); rule != nil {
+ return connc.checkSlotRule(kind, rule, false)
+ }
+ return nil
+}
+
+// Check checks whether the connection is allowed.
+func (connc *ConnectCandidate) Check() error {
+ return connc.check("connection")
+}
+
+// CheckAutoConnect checks whether the connection is allowed to auto-connect.
+func (connc *ConnectCandidate) CheckAutoConnect() error {
+ return connc.check("auto-connection")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package policy_test
+
+import (
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/interfaces/policy"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+func TestPolicy(t *testing.T) { TestingT(t) }
+
+type policySuite struct {
+ baseDecl *asserts.BaseDeclaration
+
+ plugSnap *snap.Info
+ slotSnap *snap.Info
+
+ plugDecl *asserts.SnapDeclaration
+ slotDecl *asserts.SnapDeclaration
+
+ randomSnap *snap.Info
+ randomDecl *asserts.SnapDeclaration
+}
+
+var _ = Suite(&policySuite{})
+
+func (s *policySuite) SetUpSuite(c *C) {
+ a, err := asserts.Decode([]byte(`type: base-declaration
+authority-id: canonical
+series: 16
+plugs:
+ base-plug-allow: true
+ base-plug-not-allow:
+ allow-connection: false
+ base-plug-not-allow-slots:
+ allow-connection:
+ slot-attributes:
+ s: S
+ base-plug-not-allow-plugs:
+ allow-connection:
+ plug-attributes:
+ p: P
+ base-plug-deny:
+ deny-connection: true
+ same-plug-publisher-id:
+ allow-connection:
+ slot-publisher-id:
+ - $PLUG_PUBLISHER_ID
+ plug-or:
+ allow-connection:
+ -
+ slot-attributes:
+ s: S1
+ plug-attributes:
+ p: P1
+ -
+ slot-attributes:
+ s: S2
+ plug-attributes:
+ p: P2
+ plug-on-classic-true:
+ allow-connection:
+ on-classic: true
+ plug-on-classic-distros:
+ allow-connection:
+ on-classic:
+ - ubuntu
+ - debian
+ plug-on-classic-false:
+ allow-connection:
+ on-classic: false
+ auto-base-plug-allow: true
+ auto-base-plug-not-allow:
+ allow-auto-connection: false
+ auto-base-plug-not-allow-slots:
+ allow-auto-connection:
+ slot-attributes:
+ s: S
+ auto-base-plug-not-allow-plugs:
+ allow-auto-connection:
+ plug-attributes:
+ p: P
+ auto-base-plug-deny:
+ deny-auto-connection: true
+ auto-plug-or:
+ allow-auto-connection:
+ -
+ slot-attributes:
+ s: S1
+ plug-attributes:
+ p: P1
+ -
+ slot-attributes:
+ s: S2
+ plug-attributes:
+ p: P2
+ install-plug-attr-ok:
+ allow-installation:
+ plug-attributes:
+ attr: ok
+ install-plug-gadget-only:
+ allow-installation:
+ plug-snap-type:
+ - gadget
+ install-plug-base-deny-snap-allow:
+ deny-installation:
+ plug-attributes:
+ attr: attrvalue
+ install-plug-or:
+ deny-installation:
+ -
+ plug-attributes:
+ p: P1
+ -
+ plug-snap-type:
+ - gadget
+ plug-attributes:
+ p: P2
+ install-plug-on-classic-distros:
+ allow-installation:
+ on-classic:
+ - ubuntu
+ - debian
+slots:
+ base-slot-allow: true
+ base-slot-not-allow:
+ allow-connection: false
+ base-slot-not-allow-slots:
+ allow-connection:
+ slot-attributes:
+ s: S
+ base-slot-not-allow-plugs:
+ allow-connection:
+ plug-attributes:
+ p: P
+ base-slot-deny:
+ deny-connection: true
+ base-deny-snap-slot-allow: false
+ base-deny-snap-plug-allow: false
+ base-allow-snap-slot-not-allow: true
+ gadgethelp:
+ allow-connection:
+ plug-snap-type:
+ - gadget
+ same-slot-publisher-id:
+ allow-connection:
+ plug-publisher-id:
+ - $SLOT_PUBLISHER_ID
+ slot-or:
+ allow-connection:
+ -
+ slot-attributes:
+ s: S1
+ plug-attributes:
+ p: P1
+ -
+ slot-attributes:
+ s: S2
+ plug-attributes:
+ p: P2
+ slot-on-classic-true:
+ allow-connection:
+ on-classic: true
+ slot-on-classic-distros:
+ allow-connection:
+ on-classic:
+ - ubuntu
+ - debian
+ slot-on-classic-false:
+ allow-connection:
+ on-classic: false
+ auto-base-slot-allow: true
+ auto-base-slot-not-allow:
+ allow-auto-connection: false
+ auto-base-slot-not-allow-slots:
+ allow-auto-connection:
+ slot-attributes:
+ s: S
+ auto-base-slot-not-allow-plugs:
+ allow-auto-connection:
+ plug-attributes:
+ p: P
+ auto-base-slot-deny:
+ deny-auto-connection: true
+ auto-base-deny-snap-slot-allow: false
+ auto-base-deny-snap-plug-allow: false
+ auto-base-allow-snap-slot-not-allow: true
+ auto-slot-or:
+ allow-auto-connection:
+ -
+ slot-attributes:
+ s: S1
+ plug-attributes:
+ p: P1
+ -
+ slot-attributes:
+ s: S2
+ plug-attributes:
+ p: P2
+ install-slot-coreonly:
+ allow-installation:
+ slot-snap-type:
+ - core
+ install-slot-attr-ok:
+ allow-installation:
+ slot-attributes:
+ attr: ok
+ install-slot-attr-deny:
+ deny-installation:
+ slot-attributes:
+ trust: trusted
+ install-slot-base-deny-snap-allow:
+ deny-installation:
+ slot-attributes:
+ have: true
+ install-slot-or:
+ deny-installation:
+ -
+ slot-attributes:
+ p: P1
+ -
+ slot-snap-type:
+ - gadget
+ slot-attributes:
+ p: P2
+ install-slot-on-classic-distros:
+ allow-installation:
+ on-classic:
+ - ubuntu
+ - debian
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`))
+ c.Assert(err, IsNil)
+ s.baseDecl = a.(*asserts.BaseDeclaration)
+
+ s.plugSnap = snaptest.MockInfo(c, `
+name: plug-snap
+plugs:
+ random:
+ mismatchy:
+ interface: bar
+
+ base-plug-allow:
+ base-plug-not-allow:
+ base-plug-not-allow-slots:
+ base-plug-not-allow-plugs:
+ base-plug-deny:
+
+ base-slot-allow:
+ base-slot-not-allow:
+ base-slot-not-allow-slots:
+ base-slot-not-allow-plugs:
+ base-slot-deny:
+
+ auto-base-plug-allow:
+ auto-base-plug-not-allow:
+ auto-base-plug-not-allow-slots:
+ auto-base-plug-not-allow-plugs:
+ auto-base-plug-deny:
+
+ auto-base-slot-allow:
+ auto-base-slot-not-allow:
+ auto-base-slot-not-allow-slots:
+ auto-base-slot-not-allow-plugs:
+ auto-base-slot-deny:
+
+ snap-plug-allow:
+ snap-plug-not-allow:
+ snap-plug-deny:
+
+ snap-slot-allow:
+ snap-slot-not-allow:
+ snap-slot-deny:
+
+ base-deny-snap-slot-allow:
+ base-deny-snap-plug-allow:
+ base-allow-snap-slot-not-allow:
+
+ snap-slot-deny-snap-plug-allow:
+
+ auto-snap-plug-allow:
+ auto-snap-plug-not-allow:
+ auto-snap-plug-deny:
+
+ auto-snap-slot-allow:
+ auto-snap-slot-not-allow:
+ auto-snap-slot-deny:
+
+ auto-base-deny-snap-slot-allow:
+ auto-base-deny-snap-plug-allow:
+ auto-base-allow-snap-slot-not-allow:
+
+ auto-snap-slot-deny-snap-plug-allow:
+
+ gadgethelp:
+ trustedhelp:
+
+ precise-plug-snap-id:
+ precise-slot-snap-id:
+
+ checked-plug-publisher-id:
+ checked-slot-publisher-id:
+
+ same-plug-publisher-id:
+
+ plug-or-p1-s1:
+ interface: plug-or
+ p: P1
+
+ plug-or-p2-s2:
+ interface: plug-or
+ p: P2
+
+ plug-or-p1-s2:
+ interface: plug-or
+ p: P1
+
+ auto-plug-or-p1-s1:
+ interface: auto-plug-or
+ p: P1
+
+ auto-plug-or-p2-s2:
+ interface: auto-plug-or
+ p: P2
+
+ auto-plug-or-p2-s1:
+ interface: auto-plug-or
+ p: P2
+
+ slot-or-p1-s1:
+ interface: slot-or
+ p: P1
+
+ slot-or-p2-s2:
+ interface: slot-or
+ p: P2
+
+ slot-or-p1-s2:
+ interface: slot-or
+ p: P1
+
+ auto-slot-or-p1-s1:
+ interface: auto-slot-or
+ p: P1
+
+ auto-slot-or-p2-s2:
+ interface: auto-slot-or
+ p: P2
+
+ auto-slot-or-p2-s1:
+ interface: auto-slot-or
+ p: P2
+
+ slot-on-classic-true:
+ slot-on-classic-distros:
+ slot-on-classic-false:
+
+ plug-on-classic-true:
+ plug-on-classic-distros:
+ plug-on-classic-false:
+`, nil)
+
+ s.slotSnap = snaptest.MockInfo(c, `
+name: slot-snap
+slots:
+ random:
+ mismatchy:
+ interface: baz
+
+ base-plug-allow:
+ base-plug-not-allow:
+ base-plug-not-allow-slots:
+ base-plug-not-allow-plugs:
+ base-plug-deny:
+
+ base-slot-allow:
+ base-slot-not-allow:
+ base-slot-not-allow-slots:
+ base-slot-not-allow-plugs:
+ base-slot-deny:
+
+ auto-base-plug-allow:
+ auto-base-plug-not-allow:
+ auto-base-plug-not-allow-slots:
+ auto-base-plug-not-allow-plugs:
+ auto-base-plug-deny:
+
+ auto-base-slot-allow:
+ auto-base-slot-not-allow:
+ auto-base-slot-not-allow-slots:
+ auto-base-slot-not-allow-plugs:
+ auto-base-slot-deny:
+
+ snap-plug-allow:
+ snap-plug-not-allow:
+ snap-plug-deny:
+
+ snap-slot-allow:
+ snap-slot-not-allow:
+ snap-slot-deny:
+
+ base-deny-snap-slot-allow:
+ base-deny-snap-plug-allow:
+ base-allow-snap-slot-not-allow:
+
+ snap-slot-deny-snap-plug-allow:
+
+ auto-snap-plug-allow:
+ auto-snap-plug-not-allow:
+ auto-snap-plug-deny:
+
+ auto-snap-slot-allow:
+ auto-snap-slot-not-allow:
+ auto-snap-slot-deny:
+
+ auto-base-deny-snap-slot-allow:
+ auto-base-deny-snap-plug-allow:
+ auto-base-allow-snap-slot-not-allow:
+
+ auto-snap-slot-deny-snap-plug-allow:
+
+ trustedhelp:
+
+ precise-plug-snap-id:
+ precise-slot-snap-id:
+
+ checked-plug-publisher-id:
+ checked-slot-publisher-id:
+
+ same-slot-publisher-id:
+
+ plug-or-p1-s1:
+ interface: plug-or
+ s: S1
+
+ plug-or-p2-s2:
+ interface: plug-or
+ s: S2
+
+ plug-or-p1-s2:
+ interface: plug-or
+ s: S2
+
+ auto-plug-or-p1-s1:
+ interface: auto-plug-or
+ s: S1
+
+ auto-plug-or-p2-s2:
+ interface: auto-plug-or
+ s: S2
+
+ auto-plug-or-p2-s1:
+ interface: auto-plug-or
+ s: S1
+
+ slot-or-p1-s1:
+ interface: slot-or
+ s: S1
+
+ slot-or-p2-s2:
+ interface: slot-or
+ s: S2
+
+ slot-or-p1-s2:
+ interface: slot-or
+ s: S2
+
+ auto-slot-or-p1-s1:
+ interface: auto-slot-or
+ s: S1
+
+ auto-slot-or-p2-s2:
+ interface: auto-slot-or
+ s: S2
+
+ auto-slot-or-p2-s1:
+ interface: auto-slot-or
+ s: S1
+
+ slot-on-classic-true:
+ slot-on-classic-distros:
+ slot-on-classic-false:
+
+ plug-on-classic-true:
+ plug-on-classic-distros:
+ plug-on-classic-false:
+`, nil)
+
+ a, err = asserts.Decode([]byte(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: plug-snap
+snap-id: plugsnapidididididididididididid
+publisher-id: plug-publisher
+plugs:
+ snap-plug-allow: true
+ snap-plug-deny: false
+ snap-plug-not-allow:
+ allow-connection: false
+ base-deny-snap-plug-allow: true
+ snap-slot-deny-snap-plug-allow:
+ deny-connection: false
+ trustedhelp:
+ allow-connection:
+ slot-snap-type:
+ - core
+ - gadget
+ precise-slot-snap-id:
+ allow-connection:
+ slot-snap-id:
+ - slotsnapidididididididididididid
+ checked-slot-publisher-id:
+ allow-connection:
+ slot-publisher-id:
+ - slot-publisher
+ - $PLUG_PUBLISHER_ID
+ auto-snap-plug-allow: true
+ auto-snap-plug-deny: false
+ auto-snap-plug-not-allow:
+ allow-auto-connection: false
+ auto-snap-slot-deny-snap-plug-allow:
+ deny-auto-connection: false
+ auto-base-deny-snap-plug-allow: true
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`))
+ c.Assert(err, IsNil)
+ s.plugDecl = a.(*asserts.SnapDeclaration)
+
+ a, err = asserts.Decode([]byte(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: slot-snap
+snap-id: slotsnapidididididididididididid
+publisher-id: slot-publisher
+slots:
+ snap-slot-allow: true
+ snap-slot-deny: false
+ snap-slot-not-allow:
+ allow-connection: false
+ base-deny-snap-slot-allow: true
+ snap-slot-deny-snap-plug-allow:
+ deny-connection: true
+ base-allow-snap-slot-not-allow:
+ allow-connection: false
+ precise-plug-snap-id:
+ allow-connection:
+ plug-snap-id:
+ - plugsnapidididididididididididid
+ checked-plug-publisher-id:
+ allow-connection:
+ plug-publisher-id:
+ - plug-publisher
+ auto-snap-slot-allow: true
+ auto-snap-slot-deny: false
+ auto-snap-slot-not-allow:
+ allow-auto-connection: false
+ auto-base-deny-snap-slot-allow: true
+ auto-snap-slot-deny-snap-plug-allow:
+ deny-auto-connection: true
+ auto-base-allow-snap-slot-not-allow:
+ allow-auto-connection: false
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`))
+ c.Assert(err, IsNil)
+ s.slotDecl = a.(*asserts.SnapDeclaration)
+
+ s.randomSnap = snaptest.MockInfo(c, `
+name: random-snap
+plugs:
+ precise-plug-snap-id:
+ checked-plug-publisher-id:
+ same-slot-publisher-id:
+slots:
+ precise-slot-snap-id:
+ checked-slot-publisher-id:
+ same-plug-publisher-id:
+`, nil)
+
+ a, err = asserts.Decode([]byte(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: random-snap
+snap-id: randomsnapididididididididid
+publisher-id: random-publisher
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`))
+ c.Assert(err, IsNil)
+ s.randomDecl = a.(*asserts.SnapDeclaration)
+}
+
+func (s *policySuite) TestBaselineDefaultIsAllow(c *C) {
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["random"],
+ Slot: s.slotSnap.Slots["random"],
+ BaseDeclaration: s.baseDecl,
+ }
+
+ c.Check(cand.Check(), IsNil)
+ c.Check(cand.CheckAutoConnect(), IsNil)
+}
+
+func (s *policySuite) TestInterfaceMismatch(c *C) {
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["mismatchy"],
+ Slot: s.slotSnap.Slots["mismatchy"],
+ BaseDeclaration: s.baseDecl,
+ }
+
+ c.Check(cand.Check(), ErrorMatches, `cannot connect mismatched plug interface "bar" to slot interface "baz"`)
+}
+
+func (s *policySuite) TestBaseDeclAllowDenyConnection(c *C) {
+ tests := []struct {
+ iface string
+ expected string // "" => no error
+ }{
+ {"base-plug-allow", ""},
+ {"base-plug-deny", `connection denied by plug rule of interface "base-plug-deny"`},
+ {"base-plug-not-allow", `connection not allowed by plug rule of interface "base-plug-not-allow"`},
+ {"base-slot-allow", ""},
+ {"base-slot-deny", `connection denied by slot rule of interface "base-slot-deny"`},
+ {"base-slot-not-allow", `connection not allowed by slot rule of interface "base-slot-not-allow"`},
+ {"base-plug-not-allow-slots", `connection not allowed.*`},
+ {"base-slot-not-allow-slots", `connection not allowed.*`},
+ {"base-plug-not-allow-plugs", `connection not allowed.*`},
+ {"base-slot-not-allow-plugs", `connection not allowed.*`},
+ {"plug-or-p1-s1", ""},
+ {"plug-or-p2-s2", ""},
+ {"plug-or-p1-s2", "connection not allowed by plug rule.*"},
+ {"slot-or-p1-s1", ""},
+ {"slot-or-p2-s2", ""},
+ {"slot-or-p1-s2", "connection not allowed by slot rule.*"},
+ }
+
+ for _, t := range tests {
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs[t.iface],
+ Slot: s.slotSnap.Slots[t.iface],
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err := cand.Check()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestBaseDeclAllowDenyAutoConnection(c *C) {
+ tests := []struct {
+ iface string
+ expected string // "" => no error
+ }{
+ {"auto-base-plug-allow", ""},
+ {"auto-base-plug-deny", `auto-connection denied by plug rule of interface "auto-base-plug-deny"`},
+ {"auto-base-plug-not-allow", `auto-connection not allowed by plug rule of interface "auto-base-plug-not-allow"`},
+ {"auto-base-slot-allow", ""},
+ {"auto-base-slot-deny", `auto-connection denied by slot rule of interface "auto-base-slot-deny"`},
+ {"auto-base-slot-not-allow", `auto-connection not allowed by slot rule of interface "auto-base-slot-not-allow"`},
+ {"auto-base-plug-not-allow-slots", `auto-connection not allowed.*`},
+ {"auto-base-slot-not-allow-slots", `auto-connection not allowed.*`},
+ {"auto-base-plug-not-allow-plugs", `auto-connection not allowed.*`},
+ {"auto-base-slot-not-allow-plugs", `auto-connection not allowed.*`},
+ {"auto-plug-or-p1-s1", ""},
+ {"auto-plug-or-p2-s2", ""},
+ {"auto-plug-or-p2-s1", "auto-connection not allowed by plug rule.*"},
+ {"auto-slot-or-p1-s1", ""},
+ {"auto-slot-or-p2-s2", ""},
+ {"auto-slot-or-p2-s1", "auto-connection not allowed by slot rule.*"},
+ }
+
+ for _, t := range tests {
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs[t.iface],
+ Slot: s.slotSnap.Slots[t.iface],
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err := cand.CheckAutoConnect()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestSnapDeclAllowDenyConnection(c *C) {
+ tests := []struct {
+ iface string
+ expected string // "" => no error
+ }{
+ {"random", ""},
+ {"snap-plug-allow", ""},
+ {"snap-plug-deny", `connection denied by plug rule of interface "snap-plug-deny" for "plug-snap" snap`},
+ {"snap-plug-not-allow", `connection not allowed by plug rule of interface "snap-plug-not-allow" for "plug-snap" snap`},
+ {"snap-slot-allow", ""},
+ {"snap-slot-deny", `connection denied by slot rule of interface "snap-slot-deny" for "slot-snap" snap`},
+ {"snap-slot-not-allow", `connection not allowed by slot rule of interface "snap-slot-not-allow" for "slot-snap" snap`},
+ {"base-deny-snap-slot-allow", ""},
+ {"base-deny-snap-plug-allow", ""},
+ {"snap-slot-deny-snap-plug-allow", ""},
+ {"base-allow-snap-slot-not-allow", `connection not allowed.*`},
+ }
+
+ for _, t := range tests {
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs[t.iface],
+ Slot: s.slotSnap.Slots[t.iface],
+ PlugSnapDeclaration: s.plugDecl,
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err := cand.Check()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestSnapDeclAllowDenyAutoConnection(c *C) {
+ tests := []struct {
+ iface string
+ expected string // "" => no error
+ }{
+ {"random", ""},
+ {"auto-snap-plug-allow", ""},
+ {"auto-snap-plug-deny", `auto-connection denied by plug rule of interface "auto-snap-plug-deny" for "plug-snap" snap`},
+ {"auto-snap-plug-not-allow", `auto-connection not allowed by plug rule of interface "auto-snap-plug-not-allow" for "plug-snap" snap`},
+ {"auto-snap-slot-allow", ""},
+ {"auto-snap-slot-deny", `auto-connection denied by slot rule of interface "auto-snap-slot-deny" for "slot-snap" snap`},
+ {"auto-snap-slot-not-allow", `auto-connection not allowed by slot rule of interface "auto-snap-slot-not-allow" for "slot-snap" snap`},
+ {"auto-base-deny-snap-slot-allow", ""},
+ {"auto-base-deny-snap-plug-allow", ""},
+ {"auto-snap-slot-deny-snap-plug-allow", ""},
+ {"auto-base-allow-snap-slot-not-allow", `auto-connection not allowed.*`},
+ }
+
+ for _, t := range tests {
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs[t.iface],
+ Slot: s.slotSnap.Slots[t.iface],
+ PlugSnapDeclaration: s.plugDecl,
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err := cand.CheckAutoConnect()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestSnapTypeCheckConnection(c *C) {
+ gadgetSnap := snaptest.MockInfo(c, `
+name: gadget
+type: gadget
+plugs:
+ gadgethelp:
+slots:
+ trustedhelp:
+`, nil)
+
+ coreSnap := snaptest.MockInfo(c, `
+name: core
+type: os
+slots:
+ gadgethelp:
+ trustedhelp:
+`, nil)
+
+ cand := policy.ConnectCandidate{
+ Plug: gadgetSnap.Plugs["gadgethelp"],
+ Slot: coreSnap.Slots["gadgethelp"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["gadgethelp"],
+ Slot: coreSnap.Slots["gadgethelp"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ for _, trustedSide := range []*snap.Info{coreSnap, gadgetSnap} {
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["trustedhelp"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: trustedSide.Slots["trustedhelp"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+ }
+
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["trustedhelp"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.slotSnap.Slots["trustedhelp"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+}
+
+func (s *policySuite) TestPlugSnapIDCheckConnection(c *C) {
+ // no plug-side declaration
+ cand := policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["precise-plug-snap-id"],
+ Slot: s.slotSnap.Slots["precise-plug-snap-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // plug-side declaration, wrong snap-id
+ cand = policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["precise-plug-snap-id"],
+ PlugSnapDeclaration: s.randomDecl,
+ Slot: s.slotSnap.Slots["precise-plug-snap-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // right snap-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["precise-plug-snap-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.slotSnap.Slots["precise-plug-snap-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestSlotSnapIDCheckConnection(c *C) {
+ // no slot-side declaration
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["precise-slot-snap-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.randomSnap.Slots["precise-slot-snap-id"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // slot-side declaration, wrong snap-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["precise-slot-snap-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.randomSnap.Slots["precise-slot-snap-id"],
+ SlotSnapDeclaration: s.randomDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // right snap-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["precise-slot-snap-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.slotSnap.Slots["precise-slot-snap-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestPlugPublisherIDCheckConnection(c *C) {
+ // no plug-side declaration
+ cand := policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["checked-plug-publisher-id"],
+ Slot: s.slotSnap.Slots["checked-plug-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // plug-side declaration, wrong publisher-id
+ cand = policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["checked-plug-publisher-id"],
+ PlugSnapDeclaration: s.randomDecl,
+ Slot: s.slotSnap.Slots["checked-plug-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // right publisher-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["checked-plug-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.slotSnap.Slots["checked-plug-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestSlotPublisherIDCheckConnection(c *C) {
+ // no slot-side declaration
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["checked-slot-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.randomSnap.Slots["checked-slot-publisher-id"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // slot-side declaration, wrong publisher-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["checked-slot-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.randomSnap.Slots["checked-slot-publisher-id"],
+ SlotSnapDeclaration: s.randomDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // right publisher-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["checked-slot-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.slotSnap.Slots["checked-slot-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestDollarPlugPublisherIDCheckConnection(c *C) {
+ // no known publishers
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["same-plug-publisher-id"],
+ Slot: s.randomSnap.Slots["same-plug-publisher-id"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // no slot-side declaration
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["same-plug-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.randomSnap.Slots["same-plug-publisher-id"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // slot-side declaration, wrong publisher-id
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["same-plug-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: s.randomSnap.Slots["same-plug-publisher-id"],
+ SlotSnapDeclaration: s.randomDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // slot publisher id == plug publisher id
+ samePubSlotSnap := snaptest.MockInfo(c, `
+name: same-pub-slot-snap
+slots:
+ same-plug-publisher-id:
+`, nil)
+
+ a, err := asserts.Decode([]byte(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: same-pub-slot-snap
+snap-id: samepublslotsnapidididididididid
+publisher-id: plug-publisher
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`))
+ c.Assert(err, IsNil)
+ samePubSlotDecl := a.(*asserts.SnapDeclaration)
+
+ cand = policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs["same-plug-publisher-id"],
+ PlugSnapDeclaration: s.plugDecl,
+ Slot: samePubSlotSnap.Slots["same-plug-publisher-id"],
+ SlotSnapDeclaration: samePubSlotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestDollarSlotPublisherIDCheckConnection(c *C) {
+ // no known publishers
+ cand := policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["same-slot-publisher-id"],
+ Slot: s.slotSnap.Slots["same-slot-publisher-id"],
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // no plug-side declaration
+ cand = policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["same-slot-publisher-id"],
+ Slot: s.slotSnap.Slots["same-slot-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // plug-side declaration, wrong publisher-id
+ cand = policy.ConnectCandidate{
+ Plug: s.randomSnap.Plugs["same-slot-publisher-id"],
+ PlugSnapDeclaration: s.randomDecl,
+ Slot: s.slotSnap.Slots["same-slot-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), ErrorMatches, "connection not allowed.*")
+
+ // plug publisher id == slot publisher id
+ samePubPlugSnap := snaptest.MockInfo(c, `
+name: same-pub-plug-snap
+plugs:
+ same-slot-publisher-id:
+`, nil)
+
+ a, err := asserts.Decode([]byte(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: same-pub-plug-snap
+snap-id: samepublplugsnapidididididididid
+publisher-id: slot-publisher
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`))
+ c.Assert(err, IsNil)
+ samePubPlugDecl := a.(*asserts.SnapDeclaration)
+
+ cand = policy.ConnectCandidate{
+ Plug: samePubPlugSnap.Plugs["same-slot-publisher-id"],
+ PlugSnapDeclaration: samePubPlugDecl,
+ Slot: s.slotSnap.Slots["same-slot-publisher-id"],
+ SlotSnapDeclaration: s.slotDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestBaselineDefaultIsAllowInstallation(c *C) {
+ installSnap := snaptest.MockInfo(c, `
+name: install-slot-snap
+slots:
+ random1:
+plugs:
+ random2:
+`, nil)
+
+ cand := policy.InstallCandidate{
+ Snap: installSnap,
+ BaseDeclaration: s.baseDecl,
+ }
+
+ c.Check(cand.Check(), IsNil)
+}
+
+func (s *policySuite) TestBaseDeclAllowDenyInstallation(c *C) {
+
+ tests := []struct {
+ installYaml string
+ expected string // "" => no error
+ }{
+ {`name: install-snap
+slots:
+ innocuous:
+ install-slot-coreonly:
+`, `installation not allowed by "install-slot-coreonly" slot rule of interface "install-slot-coreonly"`},
+ {`name: install-snap
+slots:
+ install-slot-attr-ok:
+ attr: ok
+`, ""},
+ {`name: install-snap
+slots:
+ install-slot-attr-deny:
+ trust: trusted
+`, `installation denied by "install-slot-attr-deny" slot rule of interface "install-slot-attr-deny"`},
+ {`name: install-snap
+plugs:
+ install-plug-attr-ok:
+ attr: ok
+`, ""},
+ {`name: install-snap
+plugs:
+ install-plug-attr-ok:
+ attr: not-ok
+`, `installation not allowed by "install-plug-attr-ok" plug rule of interface "install-plug-attr-ok"`},
+ {`name: install-snap
+plugs:
+ install-plug-gadget-only:
+`, `installation not allowed by "install-plug-gadget-only" plug rule of interface "install-plug-gadget-only"`},
+ {`name: install-gadget
+type: gadget
+plugs:
+ install-plug-gadget-only:
+`, ""},
+ {`name: install-gadget
+type: gadget
+plugs:
+ install-plug-or:
+ p: P2`, `installation denied by "install-plug-or" plug rule.*`},
+ {`name: install-snap
+plugs:
+ install-plug-or:
+ p: P1`, `installation denied by "install-plug-or" plug rule.*`},
+ {`name: install-snap
+plugs:
+ install-plug-or:
+ p: P3`, ""},
+ {`name: install-gadget
+type: gadget
+slots:
+ install-slot-or:
+ p: P2`, `installation denied by "install-slot-or" slot rule.*`},
+ {`name: install-snap
+slots:
+ install-slot-or:
+ p: P1`, `installation denied by "install-slot-or" slot rule.*`},
+ {`name: install-snap
+slots:
+ install-slot-or:
+ p: P3`, ""},
+ }
+
+ for _, t := range tests {
+ installSnap := snaptest.MockInfo(c, t.installYaml, nil)
+
+ cand := policy.InstallCandidate{
+ Snap: installSnap,
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err := cand.Check()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestSnapDeclAllowDenyInstallation(c *C) {
+
+ tests := []struct {
+ installYaml string
+ plugsSlots string
+ expected string // "" => no error
+ }{
+ {`name: install-snap
+slots:
+ install-slot-base-allow-snap-deny:
+ have: yes # bool
+`, `slots:
+ install-slot-base-allow-snap-deny:
+ deny-installation:
+ slot-attributes:
+ have: true
+`, `installation denied by "install-slot-base-allow-snap-deny" slot rule of interface "install-slot-base-allow-snap-deny" for "install-snap" snap`},
+ {`name: install-snap
+slots:
+ install-slot-base-allow-snap-not-allow:
+ have: yes # bool
+`, `slots:
+ install-slot-base-allow-snap-not-allow:
+ allow-installation:
+ slot-attributes:
+ have: false
+`, `installation not allowed by "install-slot-base-allow-snap-not-allow" slot rule of interface "install-slot-base-allow-snap-not-allow" for "install-snap" snap`},
+ {`name: install-snap
+slots:
+ install-slot-base-deny-snap-allow:
+ have: yes
+`, `slots:
+ install-slot-base-deny-snap-allow:
+ allow-installation: true
+`, ""},
+ {`name: install-snap
+plugs:
+ install-plug-base-allow-snap-deny:
+ attr: give-me
+`, `plugs:
+ install-plug-base-allow-snap-deny:
+ deny-installation:
+ plug-attributes:
+ attr: .*
+`, `installation denied by "install-plug-base-allow-snap-deny" plug rule of interface "install-plug-base-allow-snap-deny" for "install-snap" snap`},
+ {`name: install-snap
+plugs:
+ install-plug-base-allow-snap-not-allow:
+ attr: give-me
+`, `plugs:
+ install-plug-base-allow-snap-not-allow:
+ allow-installation:
+ plug-attributes:
+ attr: minimal
+`, `installation not allowed by "install-plug-base-allow-snap-not-allow" plug rule of interface "install-plug-base-allow-snap-not-allow" for "install-snap" snap`},
+ {`name: install-snap
+plugs:
+ install-plug-base-deny-snap-allow:
+ attr: attrvalue
+`, `plugs:
+ install-plug-base-deny-snap-allow:
+ allow-installation:
+ plug-attributes:
+ attr: attrvalue
+`, ""},
+ }
+
+ for _, t := range tests {
+ installSnap := snaptest.MockInfo(c, t.installYaml, nil)
+
+ a, err := asserts.Decode([]byte(strings.Replace(`type: snap-declaration
+authority-id: canonical
+series: 16
+snap-name: install-snap
+snap-id: installsnap6idididididididididid
+publisher-id: publisher
+@plugsSlots@
+timestamp: 2016-09-30T12:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw==`, "@plugsSlots@", strings.TrimSpace(t.plugsSlots), 1)))
+ c.Assert(err, IsNil)
+ snapDecl := a.(*asserts.SnapDeclaration)
+
+ cand := policy.InstallCandidate{
+ Snap: installSnap,
+ SnapDeclaration: snapDecl,
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err = cand.Check()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestPlugOnClassicCheckConnection(c *C) {
+ r1 := release.MockOnClassic(false)
+ defer r1()
+ r2 := release.MockReleaseInfo(&release.ReleaseInfo)
+ defer r2()
+
+ tests := []struct {
+ distro string // "" => not classic
+ iface string
+ err string // "" => no error
+ }{
+ {"ubuntu", "plug-on-classic-true", ""},
+ {"", "plug-on-classic-true", `connection not allowed by plug rule of interface "plug-on-classic-true"`},
+ {"", "plug-on-classic-false", ""},
+ {"ubuntu", "plug-on-classic-false", "connection not allowed.*"},
+ {"ubuntu", "plug-on-classic-distros", ""},
+ {"debian", "plug-on-classic-distros", ""},
+ {"", "plug-on-classic-distros", "connection not allowed.*"},
+ {"other", "plug-on-classic-distros", "connection not allowed.*"},
+ }
+
+ for _, t := range tests {
+ if t.distro == "" {
+ release.OnClassic = false
+ } else {
+ release.OnClassic = true
+ release.ReleaseInfo = release.OS{
+ ID: t.distro,
+ }
+ }
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs[t.iface],
+ Slot: s.slotSnap.Slots[t.iface],
+ BaseDeclaration: s.baseDecl,
+ }
+ err := cand.Check()
+ if t.err == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.err)
+ }
+ }
+}
+
+func (s *policySuite) TestSlotOnClassicCheckConnection(c *C) {
+ r1 := release.MockOnClassic(false)
+ defer r1()
+ r2 := release.MockReleaseInfo(&release.ReleaseInfo)
+ defer r2()
+
+ tests := []struct {
+ distro string // "" => not classic
+ iface string
+ err string // "" => no error
+ }{
+ {"ubuntu", "slot-on-classic-true", ""},
+ {"", "slot-on-classic-true", `connection not allowed by slot rule of interface "slot-on-classic-true"`},
+ {"", "slot-on-classic-false", ""},
+ {"ubuntu", "slot-on-classic-false", "connection not allowed.*"},
+ {"ubuntu", "slot-on-classic-distros", ""},
+ {"debian", "slot-on-classic-distros", ""},
+ {"", "slot-on-classic-distros", "connection not allowed.*"},
+ {"other", "slot-on-classic-distros", "connection not allowed.*"},
+ }
+
+ for _, t := range tests {
+ if t.distro == "" {
+ release.OnClassic = false
+ } else {
+ release.OnClassic = true
+ release.ReleaseInfo = release.OS{
+ ID: t.distro,
+ }
+ }
+ cand := policy.ConnectCandidate{
+ Plug: s.plugSnap.Plugs[t.iface],
+ Slot: s.slotSnap.Slots[t.iface],
+ BaseDeclaration: s.baseDecl,
+ }
+ err := cand.Check()
+ if t.err == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.err)
+ }
+ }
+}
+
+func (s *policySuite) TestOnClassicInstallation(c *C) {
+ r1 := release.MockOnClassic(false)
+ defer r1()
+ r2 := release.MockReleaseInfo(&release.ReleaseInfo)
+ defer r2()
+
+ tests := []struct {
+ distro string // "" => not classic
+ installYaml string
+ err string // "" => no error
+ }{
+ {"", `name: install-snap
+slots:
+ install-slot-on-classic-distros:`, `installation not allowed by "install-slot-on-classic-distros" slot rule.*`},
+ {"debian", `name: install-snap
+slots:
+ install-slot-on-classic-distros:`, ""},
+ {"", `name: install-snap
+plugs:
+ install-plug-on-classic-distros:`, `installation not allowed by "install-plug-on-classic-distros" plug rule.*`},
+ {"debian", `name: install-snap
+plugs:
+ install-plug-on-classic-distros:`, ""},
+ }
+
+ for _, t := range tests {
+ if t.distro == "" {
+ release.OnClassic = false
+ } else {
+ release.OnClassic = true
+ release.ReleaseInfo = release.OS{
+ ID: t.distro,
+ }
+ }
+
+ installSnap := snaptest.MockInfo(c, t.installYaml, nil)
+
+ cand := policy.InstallCandidate{
+ Snap: installSnap,
+ BaseDeclaration: s.baseDecl,
+ }
+ err := cand.Check()
+ if t.err == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.err)
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// Repository stores all known snappy plugs and slots and ifaces.
+type Repository struct {
+ // Protects the internals from concurrent access.
+ m sync.Mutex
+ ifaces map[string]Interface
+ // Indexed by [snapName][plugName]
+ plugs map[string]map[string]*Plug
+ slots map[string]map[string]*Slot
+ // given a slot and a plug, are they connected?
+ slotPlugs map[*Slot]map[*Plug]bool
+ // given a plug and a slot, are they connected?
+ plugSlots map[*Plug]map[*Slot]bool
+}
+
+// NewRepository creates an empty plug repository.
+func NewRepository() *Repository {
+ return &Repository{
+ ifaces: make(map[string]Interface),
+ plugs: make(map[string]map[string]*Plug),
+ slots: make(map[string]map[string]*Slot),
+ slotPlugs: make(map[*Slot]map[*Plug]bool),
+ plugSlots: make(map[*Plug]map[*Slot]bool),
+ }
+}
+
+// Interface returns an interface with a given name.
+func (r *Repository) Interface(interfaceName string) Interface {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ return r.ifaces[interfaceName]
+}
+
+// AddInterface adds the provided interface to the repository.
+func (r *Repository) AddInterface(i Interface) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ interfaceName := i.Name()
+ if err := ValidateName(interfaceName); err != nil {
+ return err
+ }
+ if _, ok := r.ifaces[interfaceName]; ok {
+ return fmt.Errorf("cannot add interface: %q, interface name is in use", interfaceName)
+ }
+ r.ifaces[interfaceName] = i
+ return nil
+}
+
+// AllPlugs returns all plugs of the given interface.
+// If interfaceName is the empty string, all plugs are returned.
+func (r *Repository) AllPlugs(interfaceName string) []*Plug {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ var result []*Plug
+ for _, plugsForSnap := range r.plugs {
+ for _, plug := range plugsForSnap {
+ if interfaceName == "" || plug.Interface == interfaceName {
+ result = append(result, plug)
+ }
+ }
+ }
+ sort.Sort(byPlugSnapAndName(result))
+ return result
+}
+
+// Plugs returns the plugs offered by the named snap.
+func (r *Repository) Plugs(snapName string) []*Plug {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ var result []*Plug
+ for _, plug := range r.plugs[snapName] {
+ result = append(result, plug)
+ }
+ sort.Sort(byPlugSnapAndName(result))
+ return result
+}
+
+// Plug returns the specified plug from the named snap.
+func (r *Repository) Plug(snapName, plugName string) *Plug {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ return r.plugs[snapName][plugName]
+}
+
+// AddPlug adds a plug to the repository.
+// Plug names must be valid snap names, as defined by ValidateName.
+// Plug name must be unique within a particular snap.
+func (r *Repository) AddPlug(plug *Plug) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ // Reject snaps with invalid names
+ if err := snap.ValidateName(plug.Snap.Name()); err != nil {
+ return err
+ }
+ // Reject plug with invalid names
+ if err := ValidateName(plug.Name); err != nil {
+ return err
+ }
+ i := r.ifaces[plug.Interface]
+ if i == nil {
+ return fmt.Errorf("cannot add plug, interface %q is not known", plug.Interface)
+ }
+ // Reject plug that don't pass interface-specific sanitization
+ if err := i.SanitizePlug(plug); err != nil {
+ return fmt.Errorf("cannot add plug: %v", err)
+ }
+ if _, ok := r.plugs[plug.Snap.Name()][plug.Name]; ok {
+ return fmt.Errorf("cannot add plug, snap %q already has plug %q", plug.Snap.Name(), plug.Name)
+ }
+ if r.plugs[plug.Snap.Name()] == nil {
+ r.plugs[plug.Snap.Name()] = make(map[string]*Plug)
+ }
+ r.plugs[plug.Snap.Name()][plug.Name] = plug
+ return nil
+}
+
+// RemovePlug removes the named plug provided by a given snap.
+// The removed plug must exist and must not be used anywhere.
+func (r *Repository) RemovePlug(snapName, plugName string) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ // Ensure that such plug exists
+ plug := r.plugs[snapName][plugName]
+ if plug == nil {
+ return fmt.Errorf("cannot remove plug %q from snap %q, no such plug", plugName, snapName)
+ }
+ // Ensure that the plug is not used by any slot
+ if len(r.plugSlots[plug]) > 0 {
+ return fmt.Errorf("cannot remove plug %q from snap %q, it is still connected", plugName, snapName)
+ }
+ delete(r.plugs[snapName], plugName)
+ if len(r.plugs[snapName]) == 0 {
+ delete(r.plugs, snapName)
+ }
+ return nil
+}
+
+// AllSlots returns all slots of the given interface.
+// If interfaceName is the empty string, all slots are returned.
+func (r *Repository) AllSlots(interfaceName string) []*Slot {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ var result []*Slot
+ for _, slotsForSnap := range r.slots {
+ for _, slot := range slotsForSnap {
+ if interfaceName == "" || slot.Interface == interfaceName {
+ result = append(result, slot)
+ }
+ }
+ }
+ sort.Sort(bySlotSnapAndName(result))
+ return result
+}
+
+// Slots returns the slots offered by the named snap.
+func (r *Repository) Slots(snapName string) []*Slot {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ var result []*Slot
+ for _, slot := range r.slots[snapName] {
+ result = append(result, slot)
+ }
+ sort.Sort(bySlotSnapAndName(result))
+ return result
+}
+
+// Slot returns the specified slot from the named snap.
+func (r *Repository) Slot(snapName, slotName string) *Slot {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ return r.slots[snapName][slotName]
+}
+
+// AddSlot adds a new slot to the repository.
+// Adding a slot with invalid name returns an error.
+// Adding a slot that has the same name and snap name as another slot returns an error.
+func (r *Repository) AddSlot(slot *Slot) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ // Reject snaps with invalid names
+ if err := snap.ValidateName(slot.Snap.Name()); err != nil {
+ return err
+ }
+ // Reject plug with invalid names
+ if err := ValidateName(slot.Name); err != nil {
+ return err
+ }
+ // TODO: ensure that apps are correct
+ i := r.ifaces[slot.Interface]
+ if i == nil {
+ return fmt.Errorf("cannot add slot, interface %q is not known", slot.Interface)
+ }
+ if err := i.SanitizeSlot(slot); err != nil {
+ return fmt.Errorf("cannot add slot: %v", err)
+ }
+ if _, ok := r.slots[slot.Snap.Name()][slot.Name]; ok {
+ return fmt.Errorf("cannot add slot, snap %q already has slot %q", slot.Snap.Name(), slot.Name)
+ }
+ if r.slots[slot.Snap.Name()] == nil {
+ r.slots[slot.Snap.Name()] = make(map[string]*Slot)
+ }
+ r.slots[slot.Snap.Name()][slot.Name] = slot
+ return nil
+}
+
+// RemoveSlot removes a named slot from the given snap.
+// Removing a slot that doesn't exist returns an error.
+// Removing a slot that is connected to a plug returns an error.
+func (r *Repository) RemoveSlot(snapName, slotName string) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ // Ensure that such slot exists
+ slot := r.slots[snapName][slotName]
+ if slot == nil {
+ return fmt.Errorf("cannot remove slot %q from snap %q, no such slot", slotName, snapName)
+ }
+ // Ensure that the slot is not using any plugs
+ if len(r.slotPlugs[slot]) > 0 {
+ return fmt.Errorf("cannot remove slot %q from snap %q, it is still connected", slotName, snapName)
+ }
+ delete(r.slots[snapName], slotName)
+ if len(r.slots[snapName]) == 0 {
+ delete(r.slots, snapName)
+ }
+ return nil
+}
+
+// ResolveConnect resolves potentially missing plug or slot names and returns a
+// fully populated connection reference.
+func (r *Repository) ResolveConnect(plugSnapName, plugName, slotSnapName, slotName string) (ConnRef, error) {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ ref := ConnRef{}
+
+ if plugSnapName == "" {
+ return ref, fmt.Errorf("cannot resolve connection, plug snap name is empty")
+ }
+ if plugName == "" {
+ return ref, fmt.Errorf("cannot resolve connection, plug name is empty")
+ }
+ // Ensure that such plug exists
+ plug := r.plugs[plugSnapName][plugName]
+ if plug == nil {
+ return ref, fmt.Errorf("snap %q has no plug named %q", plugSnapName, plugName)
+ }
+
+ if slotSnapName == "" {
+ // Use the core snap if the slot-side snap name is empty
+ switch {
+ case r.slots["core"] != nil:
+ slotSnapName = "core"
+ case r.slots["ubuntu-core"] != nil:
+ slotSnapName = "ubuntu-core"
+ default:
+ // XXX: perhaps this should not be an error and instead it should
+ // silently assume "core" now?
+ return ref, fmt.Errorf("cannot resolve connection, slot snap name is empty")
+ }
+ }
+ if slotName == "" {
+ // Find the unambiguous slot that satisfies plug requirements
+ var candidates []string
+ for candidateSlotName, candidateSlot := range r.slots[slotSnapName] {
+ // TODO: use some smarter matching (e.g. against $attrs)
+ if candidateSlot.Interface == plug.Interface {
+ candidates = append(candidates, candidateSlotName)
+ }
+ }
+ switch len(candidates) {
+ case 0:
+ return ref, fmt.Errorf("snap %q has no %q interface slots", slotSnapName, plug.Interface)
+ case 1:
+ slotName = candidates[0]
+ default:
+ sort.Strings(candidates)
+ return ref, fmt.Errorf("snap %q has multiple %q interface slots: %s", slotSnapName, plug.Interface, strings.Join(candidates, ", "))
+ }
+ }
+
+ // Ensure that such slot exists
+ slot := r.slots[slotSnapName][slotName]
+ if slot == nil {
+ return ref, fmt.Errorf("snap %q has no slot named %q", slotSnapName, slotName)
+ }
+ // Ensure that plug and slot are compatible
+ if slot.Interface != plug.Interface {
+ return ref, fmt.Errorf("cannot connect %s:%s (%q interface) to %s:%s (%q interface)",
+ plugSnapName, plugName, plug.Interface, slotSnapName, slotName, slot.Interface)
+ }
+ ref = ConnRef{PlugRef: plug.Ref(), SlotRef: slot.Ref()}
+ return ref, nil
+}
+
+// ResolveDisconnect resolves potentially missing plug or slot names and
+// returns a list of fully populated connection references that can be
+// disconnected.
+//
+// It can be used in two different ways:
+// 1: snap disconnect <snap>:<plug> <snap>:<slot>
+// 2: snap disconnect <snap>:<plug or slot>
+//
+// In the first case the referenced plug and slot must be connected. In the
+// second case any matching connection are returned but it is not an error if
+// there are no connections.
+//
+// In both cases the snap name can be omitted to implicitly refer to the core
+// snap. If there's no core snap it is simply assumed to be called "core" to
+// provide consistent error messages.
+func (r *Repository) ResolveDisconnect(plugSnapName, plugName, slotSnapName, slotName string) ([]ConnRef, error) {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ coreSnapName, _ := r.guessCoreSnapName()
+ if coreSnapName == "" {
+ // This is not strictly speaking true BUT when there's no core snap the
+ // produced error messages are consistent to when the is a core snap
+ // and it has the modern form.
+ coreSnapName = "core"
+ }
+
+ // There are two allowed forms (see snap disconnect --help)
+ switch {
+ // 1: <snap>:<plug> <snap>:<slot>
+ // Return exactly one plug/slot or an error if it doesn't exist.
+ case plugName != "" && slotName != "":
+ // The snap name can be omitted to implicitly refer to the core snap.
+ if plugSnapName == "" {
+ plugSnapName = coreSnapName
+ }
+ // Ensure that such plug exists
+ plug := r.plugs[plugSnapName][plugName]
+ if plug == nil {
+ return nil, fmt.Errorf("snap %q has no plug named %q", plugSnapName, plugName)
+ }
+ // The snap name can be omitted to implicitly refer to the core snap.
+ if slotSnapName == "" {
+ slotSnapName = coreSnapName
+ }
+ // Ensure that such slot exists
+ slot := r.slots[slotSnapName][slotName]
+ if slot == nil {
+ return nil, fmt.Errorf("snap %q has no slot named %q", slotSnapName, slotName)
+ }
+ // Ensure that slot and plug are connected
+ if !r.slotPlugs[slot][plug] {
+ return nil, fmt.Errorf("cannot disconnect %s:%s from %s:%s, it is not connected",
+ plugSnapName, plugName, slotSnapName, slotName)
+ }
+ return []ConnRef{{PlugRef: plug.Ref(), SlotRef: slot.Ref()}}, nil
+ // 2: <snap>:<plug or slot> (through 1st pair)
+ // Return a list of connections involving specified plug or slot.
+ case plugName != "" && slotName == "" && slotSnapName == "":
+ // The snap name can be omitted to implicitly refer to the core snap.
+ if plugSnapName == "" {
+ plugSnapName = coreSnapName
+ }
+ return r.connected(plugSnapName, plugName)
+ // 2: <snap>:<plug or slot> (through 2nd pair)
+ // Return a list of connections involving specified plug or slot.
+ case plugSnapName == "" && plugName == "" && slotName != "":
+ // The snap name can be omitted to implicitly refer to the core snap.
+ if slotSnapName == "" {
+ slotSnapName = coreSnapName
+ }
+ return r.connected(slotSnapName, slotName)
+ default:
+ return nil, fmt.Errorf("allowed forms are <snap>:<plug> <snap>:<slot> or <snap>:<plug or slot>")
+ }
+}
+
+// Connect establishes a connection between a plug and a slot.
+// The plug and the slot must have the same interface.
+func (r *Repository) Connect(ref ConnRef) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ plugSnapName := ref.PlugRef.Snap
+ plugName := ref.PlugRef.Name
+ slotSnapName := ref.SlotRef.Snap
+ slotName := ref.SlotRef.Name
+
+ // Ensure that such plug exists
+ plug := r.plugs[plugSnapName][plugName]
+ if plug == nil {
+ return fmt.Errorf("cannot connect plug %q from snap %q, no such plug", plugName, plugSnapName)
+ }
+ // Ensure that such slot exists
+ slot := r.slots[slotSnapName][slotName]
+ if slot == nil {
+ return fmt.Errorf("cannot connect plug to slot %q from snap %q, no such slot", slotName, slotSnapName)
+ }
+ // Ensure that plug and slot are compatible
+ if slot.Interface != plug.Interface {
+ return fmt.Errorf(`cannot connect plug "%s:%s" (interface %q) to "%s:%s" (interface %q)`,
+ plugSnapName, plugName, plug.Interface, slotSnapName, slotName, slot.Interface)
+ }
+ // Ensure that slot and plug are not connected yet
+ if r.slotPlugs[slot][plug] {
+ // But if they are don't treat this as an error.
+ return nil
+ }
+ // Connect the plug
+ if r.slotPlugs[slot] == nil {
+ r.slotPlugs[slot] = make(map[*Plug]bool)
+ }
+ if r.plugSlots[plug] == nil {
+ r.plugSlots[plug] = make(map[*Slot]bool)
+ }
+ r.slotPlugs[slot][plug] = true
+ r.plugSlots[plug][slot] = true
+ slot.Connections = append(slot.Connections, PlugRef{plug.Snap.Name(), plug.Name})
+ plug.Connections = append(plug.Connections, SlotRef{slot.Snap.Name(), slot.Name})
+ return nil
+}
+
+// Disconnect disconnects the named plug from the slot of the given snap.
+//
+// Disconnect() finds a specific slot and a specific plug and disconnects that
+// plug from that slot. It is an error if plug or slot cannot be found or if
+// the connect does not exist.
+func (r *Repository) Disconnect(plugSnapName, plugName, slotSnapName, slotName string) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ // Sanity check
+ if plugSnapName == "" {
+ return fmt.Errorf("cannot disconnect, plug snap name is empty")
+ }
+ if plugName == "" {
+ return fmt.Errorf("cannot disconnect, plug name is empty")
+ }
+ if slotSnapName == "" {
+ return fmt.Errorf("cannot disconnect, slot snap name is empty")
+ }
+ if slotName == "" {
+ return fmt.Errorf("cannot disconnect, slot name is empty")
+ }
+
+ // Ensure that such plug exists
+ plug := r.plugs[plugSnapName][plugName]
+ if plug == nil {
+ return fmt.Errorf("snap %q has no plug named %q", plugSnapName, plugName)
+ }
+ // Ensure that such slot exists
+ slot := r.slots[slotSnapName][slotName]
+ if slot == nil {
+ return fmt.Errorf("snap %q has no slot named %q", slotSnapName, slotName)
+ }
+ // Ensure that slot and plug are connected
+ if !r.slotPlugs[slot][plug] {
+ return fmt.Errorf("cannot disconnect %s:%s from %s:%s, it is not connected",
+ plugSnapName, plugName, slotSnapName, slotName)
+ }
+ r.disconnect(plug, slot)
+ return nil
+}
+
+// Connected returns references for all connections that are currently
+// established with the provided plug or slot.
+func (r *Repository) Connected(snapName, plugOrSlotName string) ([]ConnRef, error) {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ return r.connected(snapName, plugOrSlotName)
+}
+
+func (r *Repository) connected(snapName, plugOrSlotName string) ([]ConnRef, error) {
+ if snapName == "" {
+ snapName, _ = r.guessCoreSnapName()
+ if snapName == "" {
+ return nil, fmt.Errorf("snap name is empty")
+ }
+ }
+ var conns []ConnRef
+ if plugOrSlotName == "" {
+ return nil, fmt.Errorf("plug or slot name is empty")
+ }
+ // Check if plugOrSlotName actually maps to anything
+ if r.plugs[snapName][plugOrSlotName] == nil && r.slots[snapName][plugOrSlotName] == nil {
+ return nil, fmt.Errorf("snap %q has no plug or slot named %q", snapName, plugOrSlotName)
+ }
+ // Collect all the relevant connections
+ if plug, ok := r.plugs[snapName][plugOrSlotName]; ok {
+ for _, slotRef := range plug.Connections {
+ connRef := ConnRef{PlugRef: plug.Ref(), SlotRef: slotRef}
+ conns = append(conns, connRef)
+ }
+ }
+ if slot, ok := r.slots[snapName][plugOrSlotName]; ok {
+ for _, plugRef := range slot.Connections {
+ connRef := ConnRef{PlugRef: plugRef, SlotRef: slot.Ref()}
+ conns = append(conns, connRef)
+ }
+ }
+ return conns, nil
+}
+
+// coreSnapName returns the name of the core snap if one exists
+func (r *Repository) guessCoreSnapName() (string, error) {
+ switch {
+ case r.slots["core"] != nil:
+ return "core", nil
+ case r.slots["ubuntu-core"] != nil:
+ return "ubuntu-core", nil
+ default:
+ return "", fmt.Errorf("cannot guess the name of the core snap")
+ }
+}
+
+// DisconnectAll disconnects all provided connection references.
+func (r *Repository) DisconnectAll(conns []ConnRef) {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ for _, conn := range conns {
+ plug := r.plugs[conn.PlugRef.Snap][conn.PlugRef.Name]
+ slot := r.slots[conn.SlotRef.Snap][conn.SlotRef.Name]
+ if plug != nil && slot != nil {
+ r.disconnect(plug, slot)
+ }
+ }
+}
+
+// disconnect disconnects a plug from a slot.
+func (r *Repository) disconnect(plug *Plug, slot *Slot) {
+ delete(r.slotPlugs[slot], plug)
+ if len(r.slotPlugs[slot]) == 0 {
+ delete(r.slotPlugs, slot)
+ }
+ delete(r.plugSlots[plug], slot)
+ if len(r.plugSlots[plug]) == 0 {
+ delete(r.plugSlots, plug)
+ }
+ for i, plugRef := range slot.Connections {
+ if plugRef.Snap == plug.Snap.Name() && plugRef.Name == plug.Name {
+ slot.Connections[i] = slot.Connections[len(slot.Connections)-1]
+ slot.Connections = slot.Connections[:len(slot.Connections)-1]
+ if len(slot.Connections) == 0 {
+ slot.Connections = nil
+ }
+ break
+ }
+ }
+ for i, slotRef := range plug.Connections {
+ if slotRef.Snap == slot.Snap.Name() && slotRef.Name == slot.Name {
+ plug.Connections[i] = plug.Connections[len(plug.Connections)-1]
+ plug.Connections = plug.Connections[:len(plug.Connections)-1]
+ if len(plug.Connections) == 0 {
+ plug.Connections = nil
+ }
+ break
+ }
+ }
+}
+
+// Interfaces returns object holding a lists of all the plugs and slots and their connections.
+func (r *Repository) Interfaces() *Interfaces {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ ifaces := &Interfaces{}
+ // Copy and flatten plugs and slots
+ for _, plugs := range r.plugs {
+ for _, plug := range plugs {
+ p := &Plug{
+ PlugInfo: plug.PlugInfo,
+ Connections: append([]SlotRef(nil), plug.Connections...),
+ }
+ sort.Sort(bySlotRef(p.Connections))
+ ifaces.Plugs = append(ifaces.Plugs, p)
+ }
+ }
+ for _, slots := range r.slots {
+ for _, slot := range slots {
+ s := &Slot{
+ SlotInfo: slot.SlotInfo,
+ Connections: append([]PlugRef(nil), slot.Connections...),
+ }
+ sort.Sort(byPlugRef(s.Connections))
+ ifaces.Slots = append(ifaces.Slots, s)
+ }
+ }
+ sort.Sort(byPlugSnapAndName(ifaces.Plugs))
+ sort.Sort(bySlotSnapAndName(ifaces.Slots))
+ return ifaces
+}
+
+// SecuritySnippetsForSnap collects all of the snippets of a given security
+// system that affect a given snap. The return value is indexed by app/hook
+// security tag within that snap.
+func (r *Repository) SecuritySnippetsForSnap(snapName string, securitySystem SecuritySystem) (map[string][][]byte, error) {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ return r.securitySnippetsForSnap(snapName, securitySystem)
+}
+
+func addSnippet(snapName, uniqueName string, apps map[string]*snap.AppInfo, hooks map[string]*snap.HookInfo, snippets map[string][][]byte, snippet []byte) {
+ if len(snippet) == 0 {
+ return
+ }
+ for appName := range apps {
+ securityTag := snap.AppSecurityTag(snapName, appName)
+ snippets[securityTag] = append(snippets[securityTag], snippet)
+ }
+ for hookName := range hooks {
+ securityTag := snap.HookSecurityTag(snapName, hookName)
+ snippets[securityTag] = append(snippets[securityTag], snippet)
+ }
+ if len(apps) == 0 && len(hooks) == 0 {
+ securityTag := snap.NoneSecurityTag(snapName, uniqueName)
+ snippets[securityTag] = append(snippets[securityTag], snippet)
+ }
+}
+
+func (r *Repository) securitySnippetsForSnap(snapName string, securitySystem SecuritySystem) (map[string][][]byte, error) {
+ var snippets = make(map[string][][]byte)
+ // Find all of the slots that affect this snap because of plug connection.
+ for _, slot := range r.slots[snapName] {
+ iface := r.ifaces[slot.Interface]
+ // Add the static snippet for the slot
+ snippet, err := iface.PermanentSlotSnippet(slot, securitySystem)
+ if err != nil {
+ return nil, err
+ }
+ addSnippet(snapName, slot.Name, slot.Apps, nil, snippets, snippet)
+
+ // Add connection-specific snippet specific to each plug
+ for plug := range r.slotPlugs[slot] {
+ snippet, err := iface.ConnectedSlotSnippet(plug, slot, securitySystem)
+ if err != nil {
+ return nil, err
+ }
+ addSnippet(snapName, slot.Name, slot.Apps, nil, snippets, snippet)
+ }
+ }
+ // Find all of the plugs that affect this snap because of slot connection
+ for _, plug := range r.plugs[snapName] {
+ iface := r.ifaces[plug.Interface]
+ // Add the static snippet for the plug
+ snippet, err := iface.PermanentPlugSnippet(plug, securitySystem)
+ if err != nil {
+ return nil, err
+ }
+ addSnippet(snapName, plug.Name, plug.Apps, plug.Hooks, snippets, snippet)
+
+ // Add connection-specific snippet specific to each slot
+ for slot := range r.plugSlots[plug] {
+ snippet, err := iface.ConnectedPlugSnippet(plug, slot, securitySystem)
+ if err != nil {
+ return nil, err
+ }
+ addSnippet(snapName, plug.Name, plug.Apps, plug.Hooks, snippets, snippet)
+ }
+ }
+ return snippets, nil
+}
+
+// BadInterfacesError is returned when some snap interfaces could not be registered.
+// Those interfaces not mentioned in the error were successfully registered.
+type BadInterfacesError struct {
+ snap string
+ issues map[string]string // slot or plug name => message
+}
+
+func (e *BadInterfacesError) Error() string {
+ inverted := make(map[string][]string)
+ for name, reason := range e.issues {
+ inverted[reason] = append(inverted[reason], name)
+ }
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, "snap %q has bad plugs or slots: ", e.snap)
+ reasons := make([]string, 0, len(inverted))
+ for reason := range inverted {
+ reasons = append(reasons, reason)
+ }
+ sort.Strings(reasons)
+ for _, reason := range reasons {
+ names := inverted[reason]
+ sort.Strings(names)
+ for i, name := range names {
+ if i > 0 {
+ buf.WriteString(", ")
+ }
+ buf.WriteString(name)
+ }
+ fmt.Fprintf(&buf, " (%s); ", reason)
+ }
+ return strings.TrimSuffix(buf.String(), "; ")
+}
+
+// AddSnap adds plugs and slots declared by the given snap to the repository.
+//
+// This function can be used to implement snap install or, when used along with
+// RemoveSnap, snap upgrade.
+//
+// AddSnap doesn't change existing plugs/slots. The caller is responsible for
+// ensuring that the snap is not present in the repository in any way prior to
+// calling this function. If this constraint is violated then no changes are
+// made and an error is returned.
+//
+// Each added plug/slot is validated according to the corresponding interface.
+// Unknown interfaces and plugs/slots that don't validate are not added.
+// Information about those failures are returned to the caller.
+func (r *Repository) AddSnap(snapInfo *snap.Info) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ snapName := snapInfo.Name()
+
+ if r.plugs[snapName] != nil || r.slots[snapName] != nil {
+ return fmt.Errorf("cannot register interfaces for snap %q more than once", snapName)
+ }
+
+ bad := BadInterfacesError{
+ snap: snapName,
+ issues: make(map[string]string),
+ }
+
+ for plugName, plugInfo := range snapInfo.Plugs {
+ iface, ok := r.ifaces[plugInfo.Interface]
+ if !ok {
+ bad.issues[plugName] = "unknown interface"
+ continue
+ }
+ plug := &Plug{PlugInfo: plugInfo}
+ if err := iface.SanitizePlug(plug); err != nil {
+ bad.issues[plugName] = err.Error()
+ continue
+ }
+ if r.plugs[snapName] == nil {
+ r.plugs[snapName] = make(map[string]*Plug)
+ }
+ r.plugs[snapName][plugName] = plug
+ }
+
+ for slotName, slotInfo := range snapInfo.Slots {
+ iface, ok := r.ifaces[slotInfo.Interface]
+ if !ok {
+ bad.issues[slotName] = "unknown interface"
+ continue
+ }
+ slot := &Slot{SlotInfo: slotInfo}
+ if err := iface.SanitizeSlot(slot); err != nil {
+ bad.issues[slotName] = err.Error()
+ continue
+ }
+ if r.slots[snapName] == nil {
+ r.slots[snapName] = make(map[string]*Slot)
+ }
+ r.slots[snapName][slotName] = slot
+ }
+
+ if len(bad.issues) > 0 {
+ return &bad
+ }
+ return nil
+}
+
+// RemoveSnap removes all the plugs and slots associated with a given snap.
+//
+// This function can be used to implement snap removal or, when used along with
+// AddSnap, snap upgrade.
+//
+// RemoveSnap does not remove connections. The caller is responsible for
+// ensuring that connections are broken before calling this method. If this
+// constraint is violated then no changes are made and an error is returned.
+func (r *Repository) RemoveSnap(snapName string) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ for plugName, plug := range r.plugs[snapName] {
+ if len(plug.Connections) > 0 {
+ return fmt.Errorf("cannot remove connected plug %s.%s", snapName, plugName)
+ }
+ }
+ for slotName, slot := range r.slots[snapName] {
+ if len(slot.Connections) > 0 {
+ return fmt.Errorf("cannot remove connected slot %s.%s", snapName, slotName)
+ }
+ }
+
+ for _, plug := range r.plugs[snapName] {
+ delete(r.plugSlots, plug)
+ }
+ delete(r.plugs, snapName)
+ for _, slot := range r.slots[snapName] {
+ delete(r.slotPlugs, slot)
+ }
+ delete(r.slots, snapName)
+
+ return nil
+}
+
+// DisconnectSnap disconnects all the connections to and from a given snap.
+//
+// The return value is a list of names that were affected.
+func (r *Repository) DisconnectSnap(snapName string) ([]string, error) {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ seen := make(map[*snap.Info]bool)
+
+ for _, plug := range r.plugs[snapName] {
+ for slot := range r.plugSlots[plug] {
+ r.disconnect(plug, slot)
+ seen[plug.Snap] = true
+ seen[slot.Snap] = true
+ }
+ }
+
+ for _, slot := range r.slots[snapName] {
+ for plug := range r.slotPlugs[slot] {
+ r.disconnect(plug, slot)
+ seen[plug.Snap] = true
+ seen[slot.Snap] = true
+ }
+ }
+
+ result := make([]string, 0, len(seen))
+ for info := range seen {
+ result = append(result, info.Name())
+ }
+ sort.Strings(result)
+ return result, nil
+}
+
+// AutoConnectCandidates finds and returns viable auto-connection candidates
+// for a given plug.
+func (r *Repository) AutoConnectCandidates(plugSnapName, plugName string, policyCheck func(*Plug, *Slot) bool) []*Slot {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ plug := r.plugs[plugSnapName][plugName]
+ if plug == nil {
+ return nil
+ }
+
+ var candidates []*Slot
+ for _, slotsForSnap := range r.slots {
+ for _, slot := range slotsForSnap {
+ if slot.Interface != plug.Interface {
+ continue
+ }
+
+ // declaration based checks disallow
+ if !policyCheck(plug, slot) {
+ continue
+ }
+
+ if r.ifaces[plug.Interface].AutoConnect(plug, slot) {
+ candidates = append(candidates, slot)
+ }
+ }
+ }
+ return candidates
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces_test
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type RepositorySuite struct {
+ iface Interface
+ plug *Plug
+ slot *Slot
+ coreSnap *snap.Info
+ emptyRepo *Repository
+ // Repository pre-populated with s.iface
+ testRepo *Repository
+}
+
+var _ = Suite(&RepositorySuite{
+ iface: &ifacetest.TestInterface{
+ InterfaceName: "interface",
+ },
+})
+
+func (s *RepositorySuite) SetUpTest(c *C) {
+ consumer := snaptest.MockInfo(c, `
+name: consumer
+apps:
+ app:
+hooks:
+ configure:
+plugs:
+ plug:
+ interface: interface
+ label: label
+ attr: value
+`, nil)
+ s.plug = &Plug{PlugInfo: consumer.Plugs["plug"]}
+ producer := snaptest.MockInfo(c, `
+name: producer
+apps:
+ app:
+hooks:
+ configure:
+slots:
+ slot:
+ interface: interface
+ label: label
+ attr: value
+`, nil)
+ s.slot = &Slot{SlotInfo: producer.Slots["slot"]}
+ // NOTE: The core snap has a slot so that it shows up in the
+ // repository. The repository doesn't record snaps unless they
+ // have at least one interface.
+ s.coreSnap = snaptest.MockInfo(c, `
+name: core
+type: os
+slots:
+ network:
+ interface: interface
+`, nil)
+ s.emptyRepo = NewRepository()
+ s.testRepo = NewRepository()
+ err := s.testRepo.AddInterface(s.iface)
+ c.Assert(err, IsNil)
+}
+
+func addPlugsSlots(c *C, repo *Repository, yamls ...string) []*snap.Info {
+ result := make([]*snap.Info, len(yamls))
+ for i, yaml := range yamls {
+ info := snaptest.MockInfo(c, yaml, nil)
+ result[i] = info
+ for _, plugInfo := range info.Plugs {
+ err := repo.AddPlug(&Plug{PlugInfo: plugInfo})
+ c.Assert(err, IsNil)
+ }
+ for _, slotInfo := range info.Slots {
+ err := repo.AddSlot(&Slot{SlotInfo: slotInfo})
+ c.Assert(err, IsNil)
+ }
+ }
+ return result
+}
+
+// Tests for Repository.AddInterface()
+
+func (s *RepositorySuite) TestAddInterface(c *C) {
+ // Adding a valid interfaces works
+ err := s.emptyRepo.AddInterface(s.iface)
+ c.Assert(err, IsNil)
+ c.Assert(s.emptyRepo.Interface(s.iface.Name()), Equals, s.iface)
+}
+
+func (s *RepositorySuite) TestAddInterfaceClash(c *C) {
+ iface1 := &ifacetest.TestInterface{InterfaceName: "iface"}
+ iface2 := &ifacetest.TestInterface{InterfaceName: "iface"}
+ err := s.emptyRepo.AddInterface(iface1)
+ c.Assert(err, IsNil)
+ // Adding an interface with the same name as another interface is not allowed
+ err = s.emptyRepo.AddInterface(iface2)
+ c.Assert(err, ErrorMatches, `cannot add interface: "iface", interface name is in use`)
+ c.Assert(s.emptyRepo.Interface(iface1.Name()), Equals, iface1)
+}
+
+func (s *RepositorySuite) TestAddInterfaceInvalidName(c *C) {
+ iface := &ifacetest.TestInterface{InterfaceName: "bad-name-"}
+ // Adding an interface with invalid name is not allowed
+ err := s.emptyRepo.AddInterface(iface)
+ c.Assert(err, ErrorMatches, `invalid interface name: "bad-name-"`)
+ c.Assert(s.emptyRepo.Interface(iface.Name()), IsNil)
+}
+
+// Tests for Repository.Interface()
+
+func (s *RepositorySuite) TestInterface(c *C) {
+ // Interface returns nil when it cannot be found
+ iface := s.emptyRepo.Interface(s.iface.Name())
+ c.Assert(iface, IsNil)
+ c.Assert(s.emptyRepo.Interface(s.iface.Name()), IsNil)
+ err := s.emptyRepo.AddInterface(s.iface)
+ c.Assert(err, IsNil)
+ // Interface returns the found interface
+ iface = s.emptyRepo.Interface(s.iface.Name())
+ c.Assert(iface, Equals, s.iface)
+}
+
+func (s *RepositorySuite) TestInterfaceSearch(c *C) {
+ ifaceA := &ifacetest.TestInterface{InterfaceName: "a"}
+ ifaceB := &ifacetest.TestInterface{InterfaceName: "b"}
+ ifaceC := &ifacetest.TestInterface{InterfaceName: "c"}
+ err := s.emptyRepo.AddInterface(ifaceA)
+ c.Assert(err, IsNil)
+ err = s.emptyRepo.AddInterface(ifaceB)
+ c.Assert(err, IsNil)
+ err = s.emptyRepo.AddInterface(ifaceC)
+ c.Assert(err, IsNil)
+ // Interface correctly finds interfaces
+ c.Assert(s.emptyRepo.Interface("a"), Equals, ifaceA)
+ c.Assert(s.emptyRepo.Interface("b"), Equals, ifaceB)
+ c.Assert(s.emptyRepo.Interface("c"), Equals, ifaceC)
+}
+
+// Tests for Repository.AddPlug()
+
+func (s *RepositorySuite) TestAddPlug(c *C) {
+ c.Assert(s.testRepo.AllPlugs(""), HasLen, 0)
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ c.Assert(s.testRepo.AllPlugs(""), HasLen, 1)
+ c.Assert(s.testRepo.Plug(s.plug.Snap.Name(), s.plug.Name), DeepEquals, s.plug)
+}
+
+func (s *RepositorySuite) TestAddPlugClash(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddPlug(s.plug)
+ c.Assert(err, ErrorMatches, `cannot add plug, snap "consumer" already has plug "plug"`)
+ c.Assert(s.testRepo.AllPlugs(""), HasLen, 1)
+ c.Assert(s.testRepo.Plug(s.plug.Snap.Name(), s.plug.Name), DeepEquals, s.plug)
+}
+
+func (s *RepositorySuite) TestAddPlugFailsWithInvalidSnapName(c *C) {
+ plug := &Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "bad-snap-"},
+ Name: "interface",
+ Interface: "interface",
+ },
+ }
+ err := s.testRepo.AddPlug(plug)
+ c.Assert(err, ErrorMatches, `invalid snap name: "bad-snap-"`)
+ c.Assert(s.testRepo.AllPlugs(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAddPlugFailsWithInvalidPlugName(c *C) {
+ plug := &Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "snap"},
+ Name: "bad-name-",
+ Interface: "interface",
+ },
+ }
+ err := s.testRepo.AddPlug(plug)
+ c.Assert(err, ErrorMatches, `invalid interface name: "bad-name-"`)
+ c.Assert(s.testRepo.AllPlugs(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAddPlugFailsWithUnknownInterface(c *C) {
+ err := s.emptyRepo.AddPlug(s.plug)
+ c.Assert(err, ErrorMatches, `cannot add plug, interface "interface" is not known`)
+ c.Assert(s.emptyRepo.AllPlugs(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAddPlugFailsWithUnsanitizedPlug(c *C) {
+ iface := &ifacetest.TestInterface{
+ InterfaceName: "interface",
+ SanitizePlugCallback: func(plug *Plug) error {
+ return fmt.Errorf("plug is dirty")
+ },
+ }
+ err := s.emptyRepo.AddInterface(iface)
+ c.Assert(err, IsNil)
+ err = s.emptyRepo.AddPlug(s.plug)
+ c.Assert(err, ErrorMatches, "cannot add plug: plug is dirty")
+ c.Assert(s.emptyRepo.AllPlugs(""), HasLen, 0)
+}
+
+// Tests for Repository.Plug()
+
+func (s *RepositorySuite) TestPlug(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ c.Assert(s.emptyRepo.Plug(s.plug.Snap.Name(), s.plug.Name), IsNil)
+ c.Assert(s.testRepo.Plug(s.plug.Snap.Name(), s.plug.Name), DeepEquals, s.plug)
+}
+
+func (s *RepositorySuite) TestPlugSearch(c *C) {
+ addPlugsSlots(c, s.testRepo, `
+name: x
+plugs:
+ a: interface
+ b: interface
+ c: interface
+`, `
+name: y
+plugs:
+ a: interface
+ b: interface
+ c: interface
+`)
+ // Plug() correctly finds plugs
+ c.Assert(s.testRepo.Plug("x", "a"), Not(IsNil))
+ c.Assert(s.testRepo.Plug("x", "b"), Not(IsNil))
+ c.Assert(s.testRepo.Plug("x", "c"), Not(IsNil))
+ c.Assert(s.testRepo.Plug("y", "a"), Not(IsNil))
+ c.Assert(s.testRepo.Plug("y", "b"), Not(IsNil))
+ c.Assert(s.testRepo.Plug("y", "c"), Not(IsNil))
+}
+
+// Tests for Repository.RemovePlug()
+
+func (s *RepositorySuite) TestRemovePlugSucceedsWhenPlugExistsAndDisconnected(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.RemovePlug(s.plug.Snap.Name(), s.plug.Name)
+ c.Assert(err, IsNil)
+ c.Assert(s.testRepo.AllPlugs(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestRemovePlugFailsWhenPlugDoesntExist(c *C) {
+ err := s.emptyRepo.RemovePlug(s.plug.Snap.Name(), s.plug.Name)
+ c.Assert(err, ErrorMatches, `cannot remove plug "plug" from snap "consumer", no such plug`)
+}
+
+func (s *RepositorySuite) TestRemovePlugFailsWhenPlugIsConnected(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Removing a plug used by a slot returns an appropriate error
+ err = s.testRepo.RemovePlug(s.plug.Snap.Name(), s.plug.Name)
+ c.Assert(err, ErrorMatches, `cannot remove plug "plug" from snap "consumer", it is still connected`)
+ // The plug is still there
+ slot := s.testRepo.Plug(s.plug.Snap.Name(), s.plug.Name)
+ c.Assert(slot, Not(IsNil))
+}
+
+// Tests for Repository.AllPlugs()
+
+func (s *RepositorySuite) TestAllPlugsWithoutInterfaceName(c *C) {
+ snaps := addPlugsSlots(c, s.testRepo, `
+name: snap-a
+plugs:
+ name-a: interface
+`, `
+name: snap-b
+plugs:
+ name-a: interface
+ name-b: interface
+ name-c: interface
+`)
+ // The result is sorted by snap and name
+ c.Assert(s.testRepo.AllPlugs(""), DeepEquals, []*Plug{
+ {PlugInfo: snaps[0].Plugs["name-a"]},
+ {PlugInfo: snaps[1].Plugs["name-a"]},
+ {PlugInfo: snaps[1].Plugs["name-b"]},
+ {PlugInfo: snaps[1].Plugs["name-c"]},
+ })
+}
+
+func (s *RepositorySuite) TestAllPlugsWithInterfaceName(c *C) {
+ // Add another interface so that we can look for it
+ err := s.testRepo.AddInterface(&ifacetest.TestInterface{InterfaceName: "other-interface"})
+ c.Assert(err, IsNil)
+ snaps := addPlugsSlots(c, s.testRepo, `
+name: snap-a
+plugs:
+ name-a: interface
+`, `
+name: snap-b
+plugs:
+ name-a: interface
+ name-b: other-interface
+ name-c: interface
+`)
+ c.Assert(s.testRepo.AllPlugs("other-interface"), DeepEquals, []*Plug{
+ {PlugInfo: snaps[1].Plugs["name-b"]},
+ })
+}
+
+// Tests for Repository.Plugs()
+
+func (s *RepositorySuite) TestPlugs(c *C) {
+ snaps := addPlugsSlots(c, s.testRepo, `
+name: snap-a
+plugs:
+ name-a: interface
+`, `
+name: snap-b
+plugs:
+ name-a: interface
+ name-b: interface
+ name-c: interface
+`)
+ // The result is sorted by snap and name
+ c.Assert(s.testRepo.Plugs("snap-b"), DeepEquals, []*Plug{
+ {PlugInfo: snaps[1].Plugs["name-a"]},
+ {PlugInfo: snaps[1].Plugs["name-b"]},
+ {PlugInfo: snaps[1].Plugs["name-c"]},
+ })
+ // The result is empty if the snap is not known
+ c.Assert(s.testRepo.Plugs("snap-x"), HasLen, 0)
+}
+
+// Tests for Repository.AllSlots()
+
+func (s *RepositorySuite) TestAllSlots(c *C) {
+ err := s.testRepo.AddInterface(&ifacetest.TestInterface{InterfaceName: "other-interface"})
+ c.Assert(err, IsNil)
+ snaps := addPlugsSlots(c, s.testRepo, `
+name: snap-a
+slots:
+ name-a: interface
+ name-b: interface
+`, `
+name: snap-b
+slots:
+ name-a: other-interface
+`)
+ // AllSlots("") returns all slots, sorted by snap and slot name
+ c.Assert(s.testRepo.AllSlots(""), DeepEquals, []*Slot{
+ {SlotInfo: snaps[0].Slots["name-a"]},
+ {SlotInfo: snaps[0].Slots["name-b"]},
+ {SlotInfo: snaps[1].Slots["name-a"]},
+ })
+ // AllSlots("") returns all slots, sorted by snap and slot name
+ c.Assert(s.testRepo.AllSlots("other-interface"), DeepEquals, []*Slot{
+ {SlotInfo: snaps[1].Slots["name-a"]},
+ })
+}
+
+// Tests for Repository.Slots()
+
+func (s *RepositorySuite) TestSlots(c *C) {
+ snaps := addPlugsSlots(c, s.testRepo, `
+name: snap-a
+slots:
+ name-a: interface
+ name-b: interface
+`, `
+name: snap-b
+slots:
+ name-a: interface
+`)
+ // Slots("snap-a") returns slots present in that snap
+ c.Assert(s.testRepo.Slots("snap-a"), DeepEquals, []*Slot{
+ {SlotInfo: snaps[0].Slots["name-a"]},
+ {SlotInfo: snaps[0].Slots["name-b"]},
+ })
+ // Slots("snap-b") returns slots present in that snap
+ c.Assert(s.testRepo.Slots("snap-b"), DeepEquals, []*Slot{
+ {SlotInfo: snaps[1].Slots["name-a"]},
+ })
+ // Slots("snap-c") returns no slots (because that snap doesn't exist)
+ c.Assert(s.testRepo.Slots("snap-c"), HasLen, 0)
+ // Slots("") returns no slots
+ c.Assert(s.testRepo.Slots(""), HasLen, 0)
+}
+
+// Tests for Repository.Slot()
+
+func (s *RepositorySuite) TestSlotSucceedsWhenSlotExists(c *C) {
+ err := s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ slot := s.testRepo.Slot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(slot, DeepEquals, s.slot)
+}
+
+func (s *RepositorySuite) TestSlotFailsWhenSlotDoesntExist(c *C) {
+ slot := s.testRepo.Slot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(slot, IsNil)
+}
+
+// Tests for Repository.AddSlot()
+
+func (s *RepositorySuite) TestAddSlotFailsWhenInterfaceIsUnknown(c *C) {
+ err := s.emptyRepo.AddSlot(s.slot)
+ c.Assert(err, ErrorMatches, `cannot add slot, interface "interface" is not known`)
+}
+
+func (s *RepositorySuite) TestAddSlotFailsWhenSlotNameIsInvalid(c *C) {
+ slot := &Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "snap"},
+ Name: "bad-name-",
+ Interface: "interface",
+ },
+ }
+ err := s.emptyRepo.AddSlot(slot)
+ c.Assert(err, ErrorMatches, `invalid interface name: "bad-name-"`)
+ c.Assert(s.emptyRepo.AllSlots(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAddSlotFailsWithInvalidSnapName(c *C) {
+ slot := &Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "bad-snap-"},
+ Name: "slot",
+ Interface: "interface",
+ },
+ }
+ err := s.emptyRepo.AddSlot(slot)
+ c.Assert(err, ErrorMatches, `invalid snap name: "bad-snap-"`)
+ c.Assert(s.emptyRepo.AllSlots(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAddSlotFailsForDuplicates(c *C) {
+ // Adding the first slot succeeds
+ err := s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ // Adding the slot again fails with appropriate error
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, ErrorMatches, `cannot add slot, snap "producer" already has slot "slot"`)
+}
+
+func (s *RepositorySuite) TestAddSlotFailsWithUnsanitizedSlot(c *C) {
+ iface := &ifacetest.TestInterface{
+ InterfaceName: "interface",
+ SanitizeSlotCallback: func(slot *Slot) error {
+ return fmt.Errorf("slot is dirty")
+ },
+ }
+ err := s.emptyRepo.AddInterface(iface)
+ c.Assert(err, IsNil)
+ err = s.emptyRepo.AddSlot(s.slot)
+ c.Assert(err, ErrorMatches, "cannot add slot: slot is dirty")
+ c.Assert(s.emptyRepo.AllSlots(""), HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAddSlotStoresCorrectData(c *C) {
+ err := s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ slot := s.testRepo.Slot(s.slot.Snap.Name(), s.slot.Name)
+ // The added slot has the same data
+ c.Assert(slot, DeepEquals, s.slot)
+}
+
+// Tests for Repository.RemoveSlot()
+
+func (s *RepositorySuite) TestRemoveSlotSuccedsWhenSlotExistsAndDisconnected(c *C) {
+ err := s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ // Removing a vacant slot simply works
+ err = s.testRepo.RemoveSlot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, IsNil)
+ // The slot is gone now
+ slot := s.testRepo.Slot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(slot, IsNil)
+}
+
+func (s *RepositorySuite) TestRemoveSlotFailsWhenSlotDoesntExist(c *C) {
+ // Removing a slot that doesn't exist returns an appropriate error
+ err := s.testRepo.RemoveSlot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, Not(IsNil))
+ c.Assert(err, ErrorMatches, `cannot remove slot "slot" from snap "producer", no such slot`)
+}
+
+func (s *RepositorySuite) TestRemoveSlotFailsWhenSlotIsConnected(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Removing a slot occupied by a plug returns an appropriate error
+ err = s.testRepo.RemoveSlot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, ErrorMatches, `cannot remove slot "slot" from snap "producer", it is still connected`)
+ // The slot is still there
+ slot := s.testRepo.Slot(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(slot, Not(IsNil))
+}
+
+// Tests for Repository.ResolveConnect()
+
+func (s *RepositorySuite) TestResolveConnectExplicit(c *C) {
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "slot")
+ c.Check(err, IsNil)
+ c.Check(conn, Equals, ConnRef{
+ PlugRef: PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: SlotRef{Snap: "producer", Name: "slot"},
+ })
+}
+
+// ResolveConnect uses the "core" snap when slot snap name is empty
+func (s *RepositorySuite) TestResolveConnectImplicitCoreSlot(c *C) {
+ coreSnap := snaptest.MockInfo(c, `
+name: core
+type: os
+slots:
+ slot:
+ interface: interface
+`, nil)
+ c.Assert(s.testRepo.AddSnap(coreSnap), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot")
+ c.Check(err, IsNil)
+ c.Check(conn, Equals, ConnRef{
+ PlugRef: PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: SlotRef{Snap: "core", Name: "slot"},
+ })
+}
+
+// ResolveConnect uses the "ubuntu-core" snap when slot snap name is empty
+func (s *RepositorySuite) TestResolveConnectImplicitUbuntuCoreSlot(c *C) {
+ ubuntuCoreSnap := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ slot:
+ interface: interface
+`, nil)
+ c.Assert(s.testRepo.AddSnap(ubuntuCoreSnap), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot")
+ c.Check(err, IsNil)
+ c.Check(conn, Equals, ConnRef{
+ PlugRef: PlugRef{Snap: "consumer", Name: "plug"},
+ SlotRef: SlotRef{Snap: "ubuntu-core", Name: "slot"},
+ })
+}
+
+// ResolveConnect prefers the "core" snap if (by any chance) both are available
+func (s *RepositorySuite) TestResolveConnectImplicitSlotPrefersCore(c *C) {
+ coreSnap := snaptest.MockInfo(c, `
+name: core
+type: os
+slots:
+ slot:
+ interface: interface
+`, nil)
+ ubuntuCoreSnap := snaptest.MockInfo(c, `
+name: ubuntu-core
+type: os
+slots:
+ slot:
+ interface: interface
+`, nil)
+ c.Assert(s.testRepo.AddSnap(coreSnap), IsNil)
+ c.Assert(s.testRepo.AddSnap(ubuntuCoreSnap), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot")
+ c.Check(err, IsNil)
+ c.Check(conn.SlotRef.Snap, Equals, "core")
+}
+
+// ResolveConnect detects lack of candidates
+func (s *RepositorySuite) TestResolveConnectNoImplicitCandidates(c *C) {
+ err := s.testRepo.AddInterface(&ifacetest.TestInterface{InterfaceName: "other-interface"})
+ c.Assert(err, IsNil)
+ coreSnap := snaptest.MockInfo(c, `
+name: core
+type: os
+slots:
+ slot:
+ interface: other-interface
+`, nil)
+ c.Assert(s.testRepo.AddSnap(coreSnap), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "")
+ c.Check(err, ErrorMatches, `snap "core" has no "interface" interface slots`)
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// ResolveConnect detects ambiguities when slot snap name is empty
+func (s *RepositorySuite) TestResolveConnectAmbiguity(c *C) {
+ coreSnap := snaptest.MockInfo(c, `
+name: core
+type: os
+slots:
+ slot-a:
+ interface: interface
+ slot-b:
+ interface: interface
+`, nil)
+ c.Assert(s.testRepo.AddSnap(coreSnap), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "")
+ c.Check(err, ErrorMatches, `snap "core" has multiple "interface" interface slots: slot-a, slot-b`)
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Pug snap name cannot be empty
+func (s *RepositorySuite) TestResolveConnectEmptyPlugSnapName(c *C) {
+ conn, err := s.testRepo.ResolveConnect("", "plug", "producer", "slot")
+ c.Check(err, ErrorMatches, "cannot resolve connection, plug snap name is empty")
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Plug name cannot be empty
+func (s *RepositorySuite) TestResolveConnectEmptyPlugName(c *C) {
+ conn, err := s.testRepo.ResolveConnect("consumer", "", "producer", "slot")
+ c.Check(err, ErrorMatches, "cannot resolve connection, plug name is empty")
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Plug must exist
+func (s *RepositorySuite) TestResolveNoSuchPlug(c *C) {
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "consumer", "slot")
+ c.Check(err, ErrorMatches, `snap "consumer" has no plug named "plug"`)
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Slot snap name cannot be empty if there's no core snap around
+func (s *RepositorySuite) TestResolveConnectEmptySlotSnapName(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot")
+ c.Check(err, ErrorMatches, "cannot resolve connection, slot snap name is empty")
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Slot name cannot be empty if there's no core snap around
+func (s *RepositorySuite) TestResolveConnectEmptySlotName(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "")
+ c.Check(err, ErrorMatches, `snap "producer" has no "interface" interface slots`)
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Slot must exists
+func (s *RepositorySuite) TestResolveNoSuchSlot(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "slot")
+ c.Check(err, ErrorMatches, `snap "producer" has no slot named "slot"`)
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Plug and slot must have matching types
+func (s *RepositorySuite) TestResolveIncompatibleTypes(c *C) {
+ c.Assert(s.testRepo.AddInterface(&ifacetest.TestInterface{InterfaceName: "other-interface"}), IsNil)
+ plug := &Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "consumer"},
+ Name: "plug",
+ Interface: "other-interface",
+ },
+ }
+ c.Assert(s.testRepo.AddPlug(plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ // Connecting a plug to an incompatible slot fails with an appropriate error
+ conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "slot")
+ c.Check(err, ErrorMatches,
+ `cannot connect consumer:plug \("other-interface" interface\) to producer:slot \("interface" interface\)`)
+ c.Check(conn, Equals, ConnRef{})
+}
+
+// Tests for Repository.ResolveDisconnect()
+
+// All the was to resolve a 'snap disconnect' between two snaps.
+// The actual snaps are not installed though.
+func (s *RepositorySuite) TestResolveDisconnectMatrixNoSnaps(c *C) {
+ scenarios := []struct {
+ plugSnapName, plugName, slotSnapName, slotName string
+ errMsg string
+ }{
+ // Case 0 (INVALID)
+ // Nothing is provided
+ {"", "", "", "", "allowed forms are .*"},
+ // Case 1 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ // The snap name is implicit and refers to the core snap.
+ {"", "", "", "slot", `snap "core" has no plug or slot named "slot"`},
+ // Case 2 (INVALID)
+ // The slot name is not provided.
+ {"", "", "producer", "", "allowed forms are .*"},
+ // Case 3 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot
+ {"", "", "producer", "slot", `snap "producer" has no plug or slot named "slot"`},
+ // Case 4 (FAILURE)
+ // Disconnect everything from a specific plug or slot.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "", "", `snap "core" has no plug or slot named "plug"`},
+ // Case 5 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug and slot side implicit refers to the core snap.
+ {"", "plug", "", "slot", `snap "core" has no plug named "plug"`},
+ // Case 6 (INVALID)
+ // Slot name is not provided.
+ {"", "plug", "producer", "", "allowed forms are .*"},
+ // Case 7 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "producer", "slot", `snap "core" has no plug named "plug"`},
+ // Case 8 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "", "allowed forms are .*"},
+ // Case 9 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "slot", "allowed forms are .*"},
+ // Case 10 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "", "allowed forms are .*"},
+ // Case 11 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "slot", "allowed forms are .*"},
+ // Case 12 (FAILURE)
+ // Disconnect anything connected to a specific plug
+ {"consumer", "plug", "", "", `snap "consumer" has no plug or slot named "plug"`},
+ // Case 13 (FAILURE)
+ // Disconnect a specific connection.
+ // The snap name is implicit and refers to the core snap.
+ {"consumer", "plug", "", "slot", `snap "consumer" has no plug named "plug"`},
+ // Case 14 (INVALID)
+ // The slot name was not provided.
+ {"consumer", "plug", "producer", "", "allowed forms are .*"},
+ // Case 15 (FAILURE)
+ // Disconnect a specific connection.
+ {"consumer", "plug", "producer", "slot", `snap "consumer" has no plug named "plug"`},
+ }
+ for i, scenario := range scenarios {
+ c.Logf("checking scenario %d: %q", i, scenario)
+ connRefList, err := s.testRepo.ResolveDisconnect(
+ scenario.plugSnapName, scenario.plugName, scenario.slotSnapName, scenario.slotName)
+ c.Check(err, ErrorMatches, scenario.errMsg)
+ c.Check(connRefList, HasLen, 0)
+ }
+}
+
+// All the was to resolve a 'snap disconnect' between two snaps.
+// The actual snaps are not installed though but a core snap is.
+func (s *RepositorySuite) TestResolveDisconnectMatrixJustCoreSnap(c *C) {
+ c.Assert(s.testRepo.AddSnap(s.coreSnap), IsNil)
+ scenarios := []struct {
+ plugSnapName, plugName, slotSnapName, slotName string
+ errMsg string
+ }{
+ // Case 0 (INVALID)
+ // Nothing is provided
+ {"", "", "", "", "allowed forms are .*"},
+ // Case 1 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ // The snap name is implicit and refers to the core snap.
+ {"", "", "", "slot", `snap "core" has no plug or slot named "slot"`},
+ // Case 2 (INVALID)
+ // The slot name is not provided.
+ {"", "", "producer", "", "allowed forms are .*"},
+ // Case 3 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot
+ {"", "", "producer", "slot", `snap "producer" has no plug or slot named "slot"`},
+ // Case 4 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot
+ {"", "plug", "", "", `snap "core" has no plug or slot named "plug"`},
+ // Case 5 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug and slot side implicit refers to the core snap.
+ {"", "plug", "", "slot", `snap "core" has no plug named "plug"`},
+ // Case 6 (INVALID)
+ // Slot name is not provided.
+ {"", "plug", "producer", "", "allowed forms are .*"},
+ // Case 7 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "producer", "slot", `snap "core" has no plug named "plug"`},
+ // Case 8 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "", "allowed forms are .*"},
+ // Case 9 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "slot", "allowed forms are .*"},
+ // Case 10 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "", "allowed forms are .*"},
+ // Case 11 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "slot", "allowed forms are .*"},
+ // Case 12 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ {"consumer", "plug", "", "", `snap "consumer" has no plug or slot named "plug"`},
+ // Case 13 (FAILURE)
+ // Disconnect a specific connection.
+ // The snap name is implicit and refers to the core snap.
+ {"consumer", "plug", "", "slot", `snap "consumer" has no plug named "plug"`},
+ // Case 14 (INVALID)
+ // The slot name was not provided.
+ {"consumer", "plug", "producer", "", "allowed forms are .*"},
+ // Case 15 (FAILURE)
+ // Disconnect a specific connection.
+ {"consumer", "plug", "producer", "slot", `snap "consumer" has no plug named "plug"`},
+ }
+ for i, scenario := range scenarios {
+ c.Logf("checking scenario %d: %q", i, scenario)
+ connRefList, err := s.testRepo.ResolveDisconnect(
+ scenario.plugSnapName, scenario.plugName, scenario.slotSnapName, scenario.slotName)
+ c.Check(err, ErrorMatches, scenario.errMsg)
+ c.Check(connRefList, HasLen, 0)
+ }
+}
+
+// All the was to resolve a 'snap disconnect' between two snaps.
+// The actual snaps as well as the core snap are installed.
+// The snaps are not connected.
+func (s *RepositorySuite) TestResolveDisconnectMatrixDisconnectedSnaps(c *C) {
+ c.Assert(s.testRepo.AddSnap(s.coreSnap), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ scenarios := []struct {
+ plugSnapName, plugName, slotSnapName, slotName string
+ errMsg string
+ }{
+ // Case 0 (INVALID)
+ // Nothing is provided
+ {"", "", "", "", "allowed forms are .*"},
+ // Case 1 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ // The snap name is implicit and refers to the core snap.
+ {"", "", "", "slot", `snap "core" has no plug or slot named "slot"`},
+ // Case 2 (INVALID)
+ // The slot name is not provided.
+ {"", "", "producer", "", "allowed forms are .*"},
+ // Case 3 (SUCCESS)
+ // Disconnect anything connected to a specific plug or slot
+ {"", "", "producer", "slot", ""},
+ // Case 4 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "", "", `snap "core" has no plug or slot named "plug"`},
+ // Case 5 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug and slot side implicit refers to the core snap.
+ {"", "plug", "", "slot", `snap "core" has no plug named "plug"`},
+ // Case 6 (INVALID)
+ // Slot name is not provided.
+ {"", "plug", "producer", "", "allowed forms are .*"},
+ // Case 7 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "producer", "slot", `snap "core" has no plug named "plug"`},
+ // Case 8 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "", "allowed forms are .*"},
+ // Case 9 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "slot", "allowed forms are .*"},
+ // Case 10 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "", "allowed forms are .*"},
+ // Case 11 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "slot", "allowed forms are .*"},
+ // Case 12 (SUCCESS)
+ // Disconnect anything connected to a specific plug or slot.
+ {"consumer", "plug", "", "", ""},
+ // Case 13 (FAILURE)
+ // Disconnect a specific connection.
+ // The snap name is implicit and refers to the core snap.
+ {"consumer", "plug", "", "slot", `snap "core" has no slot named "slot"`},
+ // Case 14 (INVALID)
+ // The slot name was not provided.
+ {"consumer", "plug", "producer", "", "allowed forms are .*"},
+ // Case 15 (FAILURE)
+ // Disconnect a specific connection (but it is not connected).
+ {"consumer", "plug", "producer", "slot", `cannot disconnect consumer:plug from producer:slot, it is not connected`},
+ }
+ for i, scenario := range scenarios {
+ c.Logf("checking scenario %d: %q", i, scenario)
+ connRefList, err := s.testRepo.ResolveDisconnect(
+ scenario.plugSnapName, scenario.plugName, scenario.slotSnapName, scenario.slotName)
+ if scenario.errMsg != "" {
+ c.Check(err, ErrorMatches, scenario.errMsg)
+ } else {
+ c.Check(err, IsNil)
+ }
+ c.Check(connRefList, HasLen, 0)
+ }
+}
+
+// All the was to resolve a 'snap disconnect' between two snaps.
+// The actual snaps as well as the core snap are installed.
+// The snaps are connected.
+func (s *RepositorySuite) TestResolveDisconnectMatrixTypical(c *C) {
+ c.Assert(s.testRepo.AddSnap(s.coreSnap), IsNil)
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ connRef := ConnRef{s.plug.Ref(), s.slot.Ref()}
+ c.Assert(s.testRepo.Connect(connRef), IsNil)
+
+ scenarios := []struct {
+ plugSnapName, plugName, slotSnapName, slotName string
+ errMsg string
+ }{
+ // Case 0 (INVALID)
+ // Nothing is provided
+ {"", "", "", "", "allowed forms are .*"},
+ // Case 1 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ // The snap name is implicit and refers to the core snap.
+ {"", "", "", "slot", `snap "core" has no plug or slot named "slot"`},
+ // Case 2 (INVALID)
+ // The slot name is not provided.
+ {"", "", "producer", "", "allowed forms are .*"},
+ // Case 3 (SUCCESS)
+ // Disconnect anything connected to a specific plug or slot
+ {"", "", "producer", "slot", ""},
+ // Case 4 (FAILURE)
+ // Disconnect anything connected to a specific plug or slot.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "", "", `snap "core" has no plug or slot named "plug"`},
+ // Case 5 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug and slot side implicit refers to the core snap.
+ {"", "plug", "", "slot", `snap "core" has no plug named "plug"`},
+ // Case 6 (INVALID)
+ // Slot name is not provided.
+ {"", "plug", "producer", "", "allowed forms are .*"},
+ // Case 7 (FAILURE)
+ // Disconnect a specific connection.
+ // The plug side implicit refers to the core snap.
+ {"", "plug", "producer", "slot", `snap "core" has no plug named "plug"`},
+ // Case 8 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "", "allowed forms are .*"},
+ // Case 9 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "", "slot", "allowed forms are .*"},
+ // Case 10 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "", "allowed forms are .*"},
+ // Case 11 (INVALID)
+ // Plug name is not provided.
+ {"consumer", "", "producer", "slot", "allowed forms are .*"},
+ // Case 12 (SUCCESS)
+ // Disconnect anything connected to a specific plug or slot.
+ {"consumer", "plug", "", "", ""},
+ // Case 13 (FAILURE)
+ // Disconnect a specific connection.
+ // The snap name is implicit and refers to the core snap.
+ {"consumer", "plug", "", "slot", `snap "core" has no slot named "slot"`},
+ // Case 14 (INVALID)
+ // The slot name was not provided.
+ {"consumer", "plug", "producer", "", "allowed forms are .*"},
+ // Case 15 (SUCCESS)
+ // Disconnect a specific connection.
+ {"consumer", "plug", "producer", "slot", ""},
+ }
+ for i, scenario := range scenarios {
+ c.Logf("checking scenario %d: %q", i, scenario)
+ connRefList, err := s.testRepo.ResolveDisconnect(
+ scenario.plugSnapName, scenario.plugName, scenario.slotSnapName, scenario.slotName)
+ if scenario.errMsg != "" {
+ c.Check(err, ErrorMatches, scenario.errMsg)
+ c.Check(connRefList, HasLen, 0)
+ } else {
+ c.Check(err, IsNil)
+ c.Check(connRefList, DeepEquals, []ConnRef{connRef})
+ }
+ }
+}
+
+// Tests for Repository.Connect()
+
+func (s *RepositorySuite) TestConnectFailsWhenPlugDoesNotExist(c *C) {
+ err := s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ // Connecting an unknown plug returns an appropriate error
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, ErrorMatches, `cannot connect plug "plug" from snap "consumer", no such plug`)
+}
+
+func (s *RepositorySuite) TestConnectFailsWhenSlotDoesNotExist(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ // Connecting to an unknown slot returns an error
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, ErrorMatches, `cannot connect plug to slot "slot" from snap "producer", no such slot`)
+}
+
+func (s *RepositorySuite) TestConnectSucceedsWhenIdenticalConnectExists(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Connecting exactly the same thing twice succeeds without an error but does nothing.
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Only one connection is actually present.
+ c.Assert(s.testRepo.Interfaces(), DeepEquals, &Interfaces{
+ Plugs: []*Plug{{
+ PlugInfo: s.plug.PlugInfo,
+ Connections: []SlotRef{{Snap: s.slot.Snap.Name(), Name: s.slot.Name}},
+ }},
+ Slots: []*Slot{{
+ SlotInfo: s.slot.SlotInfo,
+ Connections: []PlugRef{{Snap: s.plug.Snap.Name(), Name: s.plug.Name}},
+ }},
+ })
+}
+
+func (s *RepositorySuite) TestConnectFailsWhenSlotAndPlugAreIncompatible(c *C) {
+ otherInterface := &ifacetest.TestInterface{InterfaceName: "other-interface"}
+ err := s.testRepo.AddInterface(otherInterface)
+ plug := &Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "consumer"},
+ Name: "plug",
+ Interface: "other-interface",
+ },
+ }
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddPlug(plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ // Connecting a plug to an incompatible slot fails with an appropriate error
+ connRef := ConnRef{PlugRef: PlugRef{Snap: plug.Snap.Name(), Name: plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, ErrorMatches, `cannot connect plug "consumer:plug" \(interface "other-interface"\) to "producer:slot" \(interface "interface"\)`)
+}
+
+func (s *RepositorySuite) TestConnectSucceeds(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ // Connecting a plug works okay
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, IsNil)
+}
+
+// Tests for Repository.Disconnect() and DisconnectAll()
+
+// Disconnect fails if any argument is empty
+func (s *RepositorySuite) TestDisconnectFailsOnEmptyArgs(c *C) {
+ err1 := s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), "")
+ err2 := s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, "", s.slot.Name)
+ err3 := s.testRepo.Disconnect(s.plug.Snap.Name(), "", s.slot.Snap.Name(), s.slot.Name)
+ err4 := s.testRepo.Disconnect("", s.plug.Name, s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err1, ErrorMatches, `cannot disconnect, slot name is empty`)
+ c.Assert(err2, ErrorMatches, `cannot disconnect, slot snap name is empty`)
+ c.Assert(err3, ErrorMatches, `cannot disconnect, plug name is empty`)
+ c.Assert(err4, ErrorMatches, `cannot disconnect, plug snap name is empty`)
+}
+
+// Disconnect fails if plug doesn't exist
+func (s *RepositorySuite) TestDisconnectFailsWithoutPlug(c *C) {
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ err := s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, ErrorMatches, `snap "consumer" has no plug named "plug"`)
+}
+
+// Disconnect fails if slot doesn't exist
+func (s *RepositorySuite) TestDisconnectFailsWithutSlot(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ err := s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, ErrorMatches, `snap "producer" has no slot named "slot"`)
+}
+
+// Disconnect fails if there's no connection to disconnect
+func (s *RepositorySuite) TestDisconnectFailsWhenNotConnected(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ err := s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, ErrorMatches, `cannot disconnect consumer:plug from producer:slot, it is not connected`)
+}
+
+// Disconnect works when plug and slot exist and are connected
+func (s *RepositorySuite) TestDisconnectSucceeds(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ c.Assert(s.testRepo.Connect(ConnRef{PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}), IsNil)
+ err := s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, IsNil)
+ c.Assert(s.testRepo.Interfaces(), DeepEquals, &Interfaces{
+ Plugs: []*Plug{{PlugInfo: s.plug.PlugInfo}},
+ Slots: []*Slot{{SlotInfo: s.slot.SlotInfo}},
+ })
+}
+
+// Tests for Repository.Connected
+
+// Connected fails if snap name is empty and there's no core snap around
+func (s *RepositorySuite) TestConnectedFailsWithEmptySnapName(c *C) {
+ _, err := s.testRepo.Connected("", s.plug.Name)
+ c.Check(err, ErrorMatches, "snap name is empty")
+}
+
+// Connected fails if plug or slot name is empty
+func (s *RepositorySuite) TestConnectedFailsWithEmptyPlugSlotName(c *C) {
+ _, err := s.testRepo.Connected(s.plug.Snap.Name(), "")
+ c.Check(err, ErrorMatches, "plug or slot name is empty")
+}
+
+// Connected fails if plug or slot doesn't exist
+func (s *RepositorySuite) TestConnectedFailsWithoutPlugOrSlot(c *C) {
+ _, err1 := s.testRepo.Connected(s.plug.Snap.Name(), s.plug.Name)
+ _, err2 := s.testRepo.Connected(s.slot.Snap.Name(), s.slot.Name)
+ c.Check(err1, ErrorMatches, `snap "consumer" has no plug or slot named "plug"`)
+ c.Check(err2, ErrorMatches, `snap "producer" has no plug or slot named "slot"`)
+}
+
+// Connected finds connections when asked from plug or from slot side
+func (s *RepositorySuite) TestConnectedFindsConnections(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ c.Assert(s.testRepo.Connect(ConnRef{PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}), IsNil)
+
+ conns, err := s.testRepo.Connected(s.plug.Snap.Name(), s.plug.Name)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, []ConnRef{
+ {s.plug.Ref(), s.slot.Ref()},
+ })
+
+ conns, err = s.testRepo.Connected(s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, []ConnRef{
+ {s.plug.Ref(), s.slot.Ref()},
+ })
+}
+
+// Connected uses the core snap if snap name is empty
+func (s *RepositorySuite) TestConnectedFindsCoreSnap(c *C) {
+ slot := &Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS},
+ Name: "slot",
+ Interface: "interface",
+ },
+ }
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(slot), IsNil)
+ c.Assert(s.testRepo.Connect(ConnRef{PlugRef: s.plug.Ref(), SlotRef: slot.Ref()}), IsNil)
+
+ conns, err := s.testRepo.Connected("", s.slot.Name)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, []ConnRef{
+ {s.plug.Ref(), slot.Ref()},
+ })
+}
+
+// Tests for Repository.DisconnectAll()
+
+func (s *RepositorySuite) TestDisconnectAll(c *C) {
+ c.Assert(s.testRepo.AddPlug(s.plug), IsNil)
+ c.Assert(s.testRepo.AddSlot(s.slot), IsNil)
+ c.Assert(s.testRepo.Connect(ConnRef{PlugRef: s.plug.Ref(), SlotRef: s.slot.Ref()}), IsNil)
+
+ conns := []ConnRef{{s.plug.Ref(), s.slot.Ref()}}
+ s.testRepo.DisconnectAll(conns)
+ c.Assert(s.testRepo.Interfaces(), DeepEquals, &Interfaces{
+ Plugs: []*Plug{{PlugInfo: s.plug.PlugInfo}},
+ Slots: []*Slot{{SlotInfo: s.slot.SlotInfo}},
+ })
+}
+
+// Tests for Repository.Interfaces()
+
+func (s *RepositorySuite) TestInterfacesSmokeTest(c *C) {
+ err := s.testRepo.AddPlug(s.plug)
+ c.Assert(err, IsNil)
+ err = s.testRepo.AddSlot(s.slot)
+ c.Assert(err, IsNil)
+ // After connecting the result is as expected
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = s.testRepo.Connect(connRef)
+ c.Assert(err, IsNil)
+ ifaces := s.testRepo.Interfaces()
+ c.Assert(ifaces, DeepEquals, &Interfaces{
+ Plugs: []*Plug{{
+ PlugInfo: s.plug.PlugInfo,
+ Connections: []SlotRef{{Snap: s.slot.Snap.Name(), Name: s.slot.Name}},
+ }},
+ Slots: []*Slot{{
+ SlotInfo: s.slot.SlotInfo,
+ Connections: []PlugRef{{Snap: s.plug.Snap.Name(), Name: s.plug.Name}},
+ }},
+ })
+ // After disconnecting the connections become empty
+ err = s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), s.slot.Name)
+ c.Assert(err, IsNil)
+ ifaces = s.testRepo.Interfaces()
+ c.Assert(ifaces, DeepEquals, &Interfaces{
+ Plugs: []*Plug{{PlugInfo: s.plug.PlugInfo}},
+ Slots: []*Slot{{SlotInfo: s.slot.SlotInfo}},
+ })
+}
+
+// Tests for Repository.SecuritySnippetsForSnap()
+
+const testSecurity SecuritySystem = "security"
+
+var testInterface = &ifacetest.TestInterface{
+ InterfaceName: "interface",
+ PermanentPlugSnippetCallback: func(plug *Plug, securitySystem SecuritySystem) ([]byte, error) {
+ if securitySystem == testSecurity {
+ return []byte(`static plug snippet`), nil
+ }
+ return nil, nil
+ },
+ PlugSnippetCallback: func(plug *Plug, slot *Slot, securitySystem SecuritySystem) ([]byte, error) {
+ if securitySystem == testSecurity {
+ return []byte(`connection-specific plug snippet`), nil
+ }
+ return nil, nil
+ },
+ PermanentSlotSnippetCallback: func(slot *Slot, securitySystem SecuritySystem) ([]byte, error) {
+ if securitySystem == testSecurity {
+ return []byte(`static slot snippet`), nil
+ }
+ return nil, nil
+ },
+ SlotSnippetCallback: func(plug *Plug, slot *Slot, securitySystem SecuritySystem) ([]byte, error) {
+ if securitySystem == testSecurity {
+ return []byte(`connection-specific slot snippet`), nil
+ }
+ return nil, nil
+ },
+}
+
+func (s *RepositorySuite) TestSlotSnippetsForSnapSuccess(c *C) {
+ repo := s.emptyRepo
+ c.Assert(repo.AddInterface(testInterface), IsNil)
+ c.Assert(repo.AddPlug(s.plug), IsNil)
+ c.Assert(repo.AddSlot(s.slot), IsNil)
+ // Snaps should get static security now
+ var snippets map[string][][]byte
+ snippets, err := repo.SecuritySnippetsForSnap(s.plug.Snap.Name(), testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.consumer.app": {
+ []byte(`static plug snippet`),
+ },
+ "snap.consumer.hook.configure": {
+ []byte(`static plug snippet`),
+ },
+ })
+ snippets, err = repo.SecuritySnippetsForSnap(s.slot.Snap.Name(), testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.producer.app": {
+ []byte(`static slot snippet`),
+ },
+ })
+ // Establish connection between plug and slot
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ err = repo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Snaps should get static and connection-specific security now
+ snippets, err = repo.SecuritySnippetsForSnap(s.plug.Snap.Name(), testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.consumer.app": {
+ []byte(`static plug snippet`),
+ []byte(`connection-specific plug snippet`),
+ },
+ "snap.consumer.hook.configure": {
+ []byte(`static plug snippet`),
+ []byte(`connection-specific plug snippet`),
+ },
+ })
+ snippets, err = repo.SecuritySnippetsForSnap(s.slot.Snap.Name(), testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.producer.app": {
+ []byte(`static slot snippet`),
+ []byte(`connection-specific slot snippet`),
+ },
+ })
+}
+
+func (s *RepositorySuite) TestOrphanInterfaces(c *C) {
+ repo := s.emptyRepo
+ snaps := addPlugsSlots(c, s.testRepo, `
+name: snap-a
+plugs:
+ plug-a: interface
+`, `
+name: snap-b
+slots:
+ slot-b: interface
+`)
+
+ c.Assert(repo.AddInterface(testInterface), IsNil)
+ for _, snap := range snaps {
+ err := repo.AddSnap(snap)
+ c.Assert(err, IsNil)
+ }
+
+ // Snaps should get static security now
+ var snippets map[string][][]byte
+ snippets, err := repo.SecuritySnippetsForSnap("snap-a", testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.snap-a.none.plug-a": {
+ []byte(`static plug snippet`),
+ },
+ })
+ snippets, err = repo.SecuritySnippetsForSnap("snap-b", testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.snap-b.none.slot-b": {
+ []byte(`static slot snippet`),
+ },
+ })
+
+ // Establish connection between plug and slot
+ connRef := ConnRef{PlugRef: PlugRef{Snap: "snap-a", Name: "plug-a"}, SlotRef: SlotRef{Snap: "snap-b", Name: "slot-b"}}
+ c.Assert(repo.Connect(connRef), IsNil)
+
+ // Snaps should get static and connection-specific security now
+ snippets, err = repo.SecuritySnippetsForSnap("snap-a", testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.snap-a.none.plug-a": {
+ []byte(`static plug snippet`),
+ []byte(`connection-specific plug snippet`),
+ },
+ })
+ snippets, err = repo.SecuritySnippetsForSnap("snap-b", testSecurity)
+ c.Assert(err, IsNil)
+ c.Check(snippets, DeepEquals, map[string][][]byte{
+ "snap.snap-b.none.slot-b": {
+ []byte(`static slot snippet`),
+ []byte(`connection-specific slot snippet`),
+ },
+ })
+}
+
+func (s *RepositorySuite) TestSecuritySnippetsForSnapFailureWithConnectionSnippets(c *C) {
+ var testSecurity SecuritySystem = "security"
+ iface := &ifacetest.TestInterface{
+ InterfaceName: "interface",
+ SlotSnippetCallback: func(plug *Plug, slot *Slot, securitySystem SecuritySystem) ([]byte, error) {
+ return nil, fmt.Errorf("cannot compute snippet for consumer")
+ },
+ PlugSnippetCallback: func(plug *Plug, slot *Slot, securitySystem SecuritySystem) ([]byte, error) {
+ return nil, fmt.Errorf("cannot compute snippet for provider")
+ },
+ }
+ repo := s.emptyRepo
+ c.Assert(repo.AddInterface(iface), IsNil)
+ c.Assert(repo.AddPlug(s.plug), IsNil)
+ c.Assert(repo.AddSlot(s.slot), IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ c.Assert(repo.Connect(connRef), IsNil)
+ var snippets map[string][][]byte
+ snippets, err := repo.SecuritySnippetsForSnap(s.plug.Snap.Name(), testSecurity)
+ c.Assert(err, ErrorMatches, "cannot compute snippet for provider")
+ c.Check(snippets, IsNil)
+ snippets, err = repo.SecuritySnippetsForSnap(s.slot.Snap.Name(), testSecurity)
+ c.Assert(err, ErrorMatches, "cannot compute snippet for consumer")
+ c.Check(snippets, IsNil)
+}
+
+func (s *RepositorySuite) TestSecuritySnippetsForSnapFailureWithPermanentSnippets(c *C) {
+ var testSecurity SecuritySystem = "security"
+ iface := &ifacetest.TestInterface{
+ InterfaceName: "interface",
+ PermanentSlotSnippetCallback: func(slot *Slot, securitySystem SecuritySystem) ([]byte, error) {
+ return nil, fmt.Errorf("cannot compute static snippet for consumer")
+ },
+ PermanentPlugSnippetCallback: func(plug *Plug, securitySystem SecuritySystem) ([]byte, error) {
+ return nil, fmt.Errorf("cannot compute static snippet for provider")
+ },
+ }
+ repo := s.emptyRepo
+ c.Assert(repo.AddInterface(iface), IsNil)
+ c.Assert(repo.AddPlug(s.plug), IsNil)
+ c.Assert(repo.AddSlot(s.slot), IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: s.plug.Snap.Name(), Name: s.plug.Name}, SlotRef: SlotRef{Snap: s.slot.Snap.Name(), Name: s.slot.Name}}
+ c.Assert(repo.Connect(connRef), IsNil)
+ var snippets map[string][][]byte
+ snippets, err := repo.SecuritySnippetsForSnap(s.plug.Snap.Name(), testSecurity)
+ c.Assert(err, ErrorMatches, "cannot compute static snippet for provider")
+ c.Check(snippets, IsNil)
+ snippets, err = repo.SecuritySnippetsForSnap(s.slot.Snap.Name(), testSecurity)
+ c.Assert(err, ErrorMatches, "cannot compute static snippet for consumer")
+ c.Check(snippets, IsNil)
+}
+
+func (s *RepositorySuite) TestAutoConnectCandidates(c *C) {
+ // Add two interfaces, one with automatic connections, one with manual
+ repo := s.emptyRepo
+ err := repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "auto"})
+ c.Assert(err, IsNil)
+ err = repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "manual"})
+ c.Assert(err, IsNil)
+
+ policyCheck := func(plug *Plug, slot *Slot) bool {
+ return slot.Interface == "auto"
+ }
+
+ // Add a pair of snaps with plugs/slots using those two interfaces
+ consumer := snaptest.MockInfo(c, `
+name: consumer
+plugs:
+ auto:
+ manual:
+`, nil)
+ producer := snaptest.MockInfo(c, `
+name: producer
+type: os
+slots:
+ auto:
+ manual:
+`, nil)
+ err = repo.AddSnap(producer)
+ c.Assert(err, IsNil)
+ err = repo.AddSnap(consumer)
+ c.Assert(err, IsNil)
+
+ candidateSlots := repo.AutoConnectCandidates("consumer", "auto", policyCheck)
+ c.Assert(candidateSlots, HasLen, 1)
+ c.Check(candidateSlots[0].Snap.Name(), Equals, "producer")
+ c.Check(candidateSlots[0].Interface, Equals, "auto")
+ c.Check(candidateSlots[0].Name, Equals, "auto")
+
+}
+
+// Tests for AddSnap and RemoveSnap
+
+type AddRemoveSuite struct {
+ repo *Repository
+}
+
+var _ = Suite(&AddRemoveSuite{})
+
+func (s *AddRemoveSuite) SetUpTest(c *C) {
+ s.repo = NewRepository()
+ err := s.repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "iface"})
+ c.Assert(err, IsNil)
+ err = s.repo.AddInterface(&ifacetest.TestInterface{
+ InterfaceName: "invalid",
+ SanitizePlugCallback: func(plug *Plug) error { return fmt.Errorf("plug is invalid") },
+ SanitizeSlotCallback: func(slot *Slot) error { return fmt.Errorf("slot is invalid") },
+ })
+ c.Assert(err, IsNil)
+}
+
+func (s *AddRemoveSuite) TestAddSnapComplexErrorHandling(c *C) {
+ err := s.repo.AddInterface(&ifacetest.TestInterface{
+ InterfaceName: "invalid-plug-iface",
+ SanitizePlugCallback: func(plug *Plug) error { return fmt.Errorf("plug is invalid") },
+ SanitizeSlotCallback: func(slot *Slot) error { return fmt.Errorf("slot is invalid") },
+ })
+ c.Assert(err, IsNil)
+ err = s.repo.AddInterface(&ifacetest.TestInterface{
+ InterfaceName: "invalid-slot-iface",
+ SanitizePlugCallback: func(plug *Plug) error { return fmt.Errorf("plug is invalid") },
+ SanitizeSlotCallback: func(slot *Slot) error { return fmt.Errorf("slot is invalid") },
+ })
+ c.Assert(err, IsNil)
+ snapInfo := snaptest.MockInfo(c, `
+name: complex
+plugs:
+ invalid-plug-iface:
+ unknown-plug-iface:
+slots:
+ invalid-slot-iface:
+ unknown-slot-iface:
+`, nil)
+ err = s.repo.AddSnap(snapInfo)
+ c.Check(err, ErrorMatches,
+ `snap "complex" has bad plugs or slots: invalid-plug-iface \(plug is invalid\); invalid-slot-iface \(slot is invalid\); unknown-plug-iface, unknown-slot-iface \(unknown interface\)`)
+ // Nothing was added
+ c.Check(s.repo.Plug("complex", "invalid-plug-iface"), IsNil)
+ c.Check(s.repo.Plug("complex", "unknown-plug-iface"), IsNil)
+ c.Check(s.repo.Slot("complex", "invalid-slot-iface"), IsNil)
+ c.Check(s.repo.Slot("complex", "unknown-slot-iface"), IsNil)
+}
+
+const testConsumerYaml = `
+name: consumer
+apps:
+ app:
+ plugs: [iface]
+`
+const testProducerYaml = `
+name: producer
+apps:
+ app:
+ slots: [iface]
+`
+
+func (s *AddRemoveSuite) addSnap(c *C, yaml string) (*snap.Info, error) {
+ snapInfo := snaptest.MockInfo(c, yaml, nil)
+ return snapInfo, s.repo.AddSnap(snapInfo)
+}
+
+func (s *AddRemoveSuite) TestAddSnapAddsPlugs(c *C) {
+ _, err := s.addSnap(c, testConsumerYaml)
+ c.Assert(err, IsNil)
+ // The plug was added
+ c.Assert(s.repo.Plug("consumer", "iface"), Not(IsNil))
+}
+
+func (s *AddRemoveSuite) TestAddSnapErrorsOnExistingSnapPlugs(c *C) {
+ _, err := s.addSnap(c, testConsumerYaml)
+ c.Assert(err, IsNil)
+ _, err = s.addSnap(c, testConsumerYaml)
+ c.Assert(err, ErrorMatches, `cannot register interfaces for snap "consumer" more than once`)
+}
+
+func (s *AddRemoveSuite) TestAddSnapAddsSlots(c *C) {
+ _, err := s.addSnap(c, testProducerYaml)
+ c.Assert(err, IsNil)
+ // The slot was added
+ c.Assert(s.repo.Slot("producer", "iface"), Not(IsNil))
+}
+
+func (s *AddRemoveSuite) TestAddSnapErrorsOnExistingSnapSlots(c *C) {
+ _, err := s.addSnap(c, testProducerYaml)
+ c.Assert(err, IsNil)
+ _, err = s.addSnap(c, testProducerYaml)
+ c.Assert(err, ErrorMatches, `cannot register interfaces for snap "producer" more than once`)
+}
+
+func (s AddRemoveSuite) TestRemoveRemovesPlugs(c *C) {
+ _, err := s.addSnap(c, testConsumerYaml)
+ c.Assert(err, IsNil)
+ s.repo.RemoveSnap("consumer")
+ c.Assert(s.repo.Plug("consumer", "iface"), IsNil)
+}
+
+func (s AddRemoveSuite) TestRemoveRemovesSlots(c *C) {
+ _, err := s.addSnap(c, testProducerYaml)
+ c.Assert(err, IsNil)
+ s.repo.RemoveSnap("producer")
+ c.Assert(s.repo.Plug("producer", "iface"), IsNil)
+}
+
+func (s *AddRemoveSuite) TestRemoveSnapErrorsOnStillConnectedPlug(c *C) {
+ _, err := s.addSnap(c, testConsumerYaml)
+ c.Assert(err, IsNil)
+ _, err = s.addSnap(c, testProducerYaml)
+ c.Assert(err, IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "iface"}, SlotRef: SlotRef{Snap: "producer", Name: "iface"}}
+ err = s.repo.Connect(connRef)
+ c.Assert(err, IsNil)
+ err = s.repo.RemoveSnap("consumer")
+ c.Assert(err, ErrorMatches, "cannot remove connected plug consumer.iface")
+}
+
+func (s *AddRemoveSuite) TestRemoveSnapErrorsOnStillConnectedSlot(c *C) {
+ _, err := s.addSnap(c, testConsumerYaml)
+ c.Assert(err, IsNil)
+ _, err = s.addSnap(c, testProducerYaml)
+ c.Assert(err, IsNil)
+ connRef := ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "iface"}, SlotRef: SlotRef{Snap: "producer", Name: "iface"}}
+ err = s.repo.Connect(connRef)
+ c.Assert(err, IsNil)
+ err = s.repo.RemoveSnap("producer")
+ c.Assert(err, ErrorMatches, "cannot remove connected slot producer.iface")
+}
+
+type DisconnectSnapSuite struct {
+ repo *Repository
+ s1, s2 *snap.Info
+}
+
+var _ = Suite(&DisconnectSnapSuite{})
+
+func (s *DisconnectSnapSuite) SetUpTest(c *C) {
+ s.repo = NewRepository()
+
+ err := s.repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "iface-a"})
+ c.Assert(err, IsNil)
+ err = s.repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "iface-b"})
+ c.Assert(err, IsNil)
+
+ s.s1 = snaptest.MockInfo(c, `
+name: s1
+plugs:
+ iface-a:
+slots:
+ iface-b:
+`, nil)
+ err = s.repo.AddSnap(s.s1)
+ c.Assert(err, IsNil)
+
+ s.s2 = snaptest.MockInfo(c, `
+name: s2
+plugs:
+ iface-b:
+slots:
+ iface-a:
+`, nil)
+ c.Assert(err, IsNil)
+ err = s.repo.AddSnap(s.s2)
+ c.Assert(err, IsNil)
+}
+
+func (s *DisconnectSnapSuite) TestNotConnected(c *C) {
+ affected, err := s.repo.DisconnectSnap("s1")
+ c.Assert(err, IsNil)
+ c.Check(affected, HasLen, 0)
+}
+
+func (s *DisconnectSnapSuite) TestOutgoingConnection(c *C) {
+ connRef := ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "iface-a"}, SlotRef: SlotRef{Snap: "s2", Name: "iface-a"}}
+ err := s.repo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Disconnect s1 with which has an outgoing connection to s2
+ affected, err := s.repo.DisconnectSnap("s1")
+ c.Assert(err, IsNil)
+ c.Check(affected, testutil.Contains, "s1")
+ c.Check(affected, testutil.Contains, "s2")
+}
+
+func (s *DisconnectSnapSuite) TestIncomingConnection(c *C) {
+ connRef := ConnRef{PlugRef: PlugRef{Snap: "s2", Name: "iface-b"}, SlotRef: SlotRef{Snap: "s1", Name: "iface-b"}}
+ err := s.repo.Connect(connRef)
+ c.Assert(err, IsNil)
+ // Disconnect s1 with which has an incoming connection from s2
+ affected, err := s.repo.DisconnectSnap("s1")
+ c.Assert(err, IsNil)
+ c.Check(affected, testutil.Contains, "s1")
+ c.Check(affected, testutil.Contains, "s2")
+}
+
+func (s *DisconnectSnapSuite) TestCrossConnection(c *C) {
+ // This test is symmetric wrt s1 <-> s2 connections
+ for _, snapName := range []string{"s1", "s2"} {
+ connRef1 := ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "iface-a"}, SlotRef: SlotRef{Snap: "s2", Name: "iface-a"}}
+ err := s.repo.Connect(connRef1)
+ c.Assert(err, IsNil)
+ connRef2 := ConnRef{PlugRef: PlugRef{Snap: "s2", Name: "iface-b"}, SlotRef: SlotRef{Snap: "s1", Name: "iface-b"}}
+ err = s.repo.Connect(connRef2)
+ c.Assert(err, IsNil)
+ affected, err := s.repo.DisconnectSnap(snapName)
+ c.Assert(err, IsNil)
+ c.Check(affected, testutil.Contains, "s1")
+ c.Check(affected, testutil.Contains, "s2")
+ }
+}
+
+func contentPolicyCheck(plug *Plug, slot *Slot) bool {
+ return plug.Snap.PublisherID == slot.Snap.PublisherID
+}
+
+func contentAutoConnect(plug *Plug, slot *Slot) bool {
+ return plug.Attrs["content"] == slot.Attrs["content"]
+}
+
+// internal helper that creates a new repository with two snaps, one
+// is a content plug and one a content slot
+func makeContentConnectionTestSnaps(c *C, plugContentToken, slotContentToken string) (*Repository, *snap.Info, *snap.Info) {
+ repo := NewRepository()
+ err := repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "content", AutoConnectCallback: contentAutoConnect})
+ c.Assert(err, IsNil)
+
+ plugSnap := snaptest.MockInfo(c, fmt.Sprintf(`
+name: content-plug-snap
+plugs:
+ import-content:
+ interface: content
+ content: %s
+`, plugContentToken), nil)
+ slotSnap := snaptest.MockInfo(c, fmt.Sprintf(`
+name: content-slot-snap
+slots:
+ exported-content:
+ interface: content
+ content: %s
+`, slotContentToken), nil)
+
+ err = repo.AddSnap(plugSnap)
+ c.Assert(err, IsNil)
+ err = repo.AddSnap(slotSnap)
+ c.Assert(err, IsNil)
+
+ return repo, plugSnap, slotSnap
+}
+
+func (s *RepositorySuite) TestAutoConnectContentInterfaceSimple(c *C) {
+ repo, _, _ := makeContentConnectionTestSnaps(c, "mylib", "mylib")
+ candidateSlots := repo.AutoConnectCandidates("content-plug-snap", "import-content", contentPolicyCheck)
+ c.Assert(candidateSlots, HasLen, 1)
+ c.Check(candidateSlots[0].Name, Equals, "exported-content")
+}
+
+func (s *RepositorySuite) TestAutoConnectContentInterfaceOSWorksCorrectly(c *C) {
+ repo, _, slotSnap := makeContentConnectionTestSnaps(c, "mylib", "otherlib")
+ slotSnap.Type = snap.TypeOS
+
+ candidateSlots := repo.AutoConnectCandidates("content-plug-snap", "import-content", contentPolicyCheck)
+ c.Check(candidateSlots, HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAutoConnectContentInterfaceNoMatchingContent(c *C) {
+ repo, _, _ := makeContentConnectionTestSnaps(c, "mylib", "otherlib")
+ candidateSlots := repo.AutoConnectCandidates("content-plug-snap", "import-content", contentPolicyCheck)
+ c.Check(candidateSlots, HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAutoConnectContentInterfaceNoMatchingDeveloper(c *C) {
+ repo, plugSnap, slotSnap := makeContentConnectionTestSnaps(c, "mylib", "mylib")
+ // real code will use the assertions, this is just for emulation
+ plugSnap.PublisherID = "fooid"
+ slotSnap.PublisherID = "barid"
+
+ candidateSlots := repo.AutoConnectCandidates("content-plug-snap", "import-content", contentPolicyCheck)
+ c.Check(candidateSlots, HasLen, 0)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package seccomp implements integration between snappy and
+// ubuntu-core-launcher around seccomp.
+//
+// Snappy creates so-called seccomp profiles for each application (for each
+// snap) present in the system. Upon each execution of ubuntu-core-launcher,
+// the profile is read and "compiled" to an eBPF program and injected into the
+// kernel for the duration of the execution of the process.
+//
+// There is no binary cache for seccomp, each time the launcher starts an
+// application the profile is parsed and re-compiled.
+//
+// The actual profiles are stored in /var/lib/snappy/seccomp/profiles.
+// This directory is hard-coded in ubuntu-core-launcher.
+package seccomp
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining seccomp profiles for ubuntu-core-launcher.
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "seccomp"
+}
+
+// Setup creates seccomp profiles specific to a given snap.
+// The snap can be in developer mode to make security violations non-fatal to
+// the offending application process.
+//
+// This method should be called after changing plug, slots, connections between
+// them or application present in the snap.
+func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ // Get the snippets that apply to this snap
+ snippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecuritySecComp)
+ if err != nil {
+ return fmt.Errorf("cannot obtain security snippets for snap %q: %s", snapName, err)
+ }
+ // Get the files that this snap should have
+ content, err := b.combineSnippets(snapInfo, opts, snippets)
+ if err != nil {
+ return fmt.Errorf("cannot obtain expected security files for snap %q: %s", snapName, err)
+ }
+ glob := interfaces.SecurityTagGlob(snapName)
+ dir := dirs.SnapSeccompDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for seccomp profiles %q: %s", dir, err)
+ }
+ _, _, err = osutil.EnsureDirState(dir, glob, content)
+ if err != nil {
+ return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, err)
+ }
+ return nil
+}
+
+// Remove removes seccomp profiles of a given snap.
+func (b *Backend) Remove(snapName string) error {
+ glob := interfaces.SecurityTagGlob(snapName)
+ _, _, err := osutil.EnsureDirState(dirs.SnapSeccompDir, glob, nil)
+ if err != nil {
+ return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, err)
+ }
+ return nil
+}
+
+// combineSnippets combines security snippets collected from all the interfaces
+// affecting a given snap into a content map applicable to EnsureDirState.
+func (b *Backend) combineSnippets(snapInfo *snap.Info, opts interfaces.ConfinementOptions, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) {
+ for _, appInfo := range snapInfo.Apps {
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+ addContent(appInfo.SecurityTag(), opts, snippets, content)
+ }
+
+ for _, hookInfo := range snapInfo.Hooks {
+ if content == nil {
+ content = make(map[string]*osutil.FileState)
+ }
+ addContent(hookInfo.SecurityTag(), opts, snippets, content)
+ }
+
+ return content, nil
+}
+
+func addContent(securityTag string, opts interfaces.ConfinementOptions, snippets map[string][][]byte, content map[string]*osutil.FileState) {
+ var buffer bytes.Buffer
+ if opts.Classic && !opts.JailMode {
+ // NOTE: This is understood by snap-confine
+ buffer.WriteString("@unrestricted\n")
+ }
+ if opts.DevMode && !opts.JailMode {
+ // NOTE: This is understood by snap-confine
+ buffer.WriteString("@complain\n")
+ }
+
+ buffer.Write(defaultTemplate)
+ for _, snippet := range snippets[securityTag] {
+ buffer.Write(snippet)
+ buffer.WriteRune('\n')
+ }
+
+ content[securityTag] = &osutil.FileState{
+ Content: buffer.Bytes(),
+ Mode: 0644,
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package seccomp_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+}
+
+var _ = Suite(&backendSuite{})
+
+var testedConfinementOpts = []interfaces.ConfinementOptions{
+ {},
+ {DevMode: true},
+ {JailMode: true},
+ {Classic: true},
+}
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.Backend = &seccomp.Backend{}
+ s.BackendSuite.SetUpTest(c)
+
+ // Prepare a directory for seccomp profiles.
+ // NOTE: Normally this is a part of the OS snap.
+ err := os.MkdirAll(dirs.SnapSeccompDir, 0700)
+ c.Assert(err, IsNil)
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.BackendSuite.TearDownTest(c)
+}
+
+// Tests for Setup() and Remove()
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "seccomp")
+}
+
+func (s *backendSuite) TestInstallingSnapWritesProfiles(c *C) {
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.smbd")
+ // file called "snap.sambda.smbd" was created
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+}
+
+func (s *backendSuite) TestInstallingSnapWritesHookProfiles(c *C) {
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, ifacetest.HookYaml, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.foo.hook.configure")
+
+ // Verify that profile named "snap.foo.hook.configure" was created.
+ _, err := os.Stat(profile)
+ c.Check(err, IsNil)
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesProfiles(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.RemoveSnap(c, snapInfo)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.smbd")
+ // file called "snap.sambda.smbd" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesHookProfiles(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.HookYaml, 0)
+ s.RemoveSnap(c, snapInfo)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.foo.hook.configure")
+
+ // Verify that profile "snap.foo.hook.configure" was removed.
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.nmbd")
+ _, err := os.Stat(profile)
+ // file called "snap.sambda.nmbd" was created
+ c.Check(err, IsNil)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithHooks(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlWithHook, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.hook.configure")
+
+ _, err := os.Stat(profile)
+ // Verify that profile "snap.samba.hook.configure" was created.
+ c.Check(err, IsNil)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1WithNmbd, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.nmbd")
+ // file called "snap.sambda.nmbd" was removed
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithNoHooks(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlWithHook, 0)
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.hook.configure")
+
+ // Verify that profile snap.samba.hook.configure was removed.
+ _, err := os.Stat(profile)
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestRealDefaultTemplateIsNormallyUsed(c *C) {
+ snapInfo := snaptest.MockInfo(c, ifacetest.SambaYamlV1, nil)
+ // NOTE: we don't call seccomp.MockTemplate()
+ err := s.Backend.Setup(snapInfo, interfaces.ConfinementOptions{}, s.Repo)
+ c.Assert(err, IsNil)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.smbd")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ for _, line := range []string{
+ // NOTE: a few randomly picked lines from the real profile. Comments
+ // and empty lines are avoided as those can be discarded in the future.
+ "deny init_module\n",
+ "open\n",
+ "getuid\n",
+ } {
+ c.Assert(string(data), testutil.Contains, line)
+ }
+}
+
+type combineSnippetsScenario struct {
+ opts interfaces.ConfinementOptions
+ snippet string
+ content string
+}
+
+var combineSnippetsScenarios = []combineSnippetsScenario{{
+ opts: interfaces.ConfinementOptions{},
+ content: "default\n",
+}, {
+ opts: interfaces.ConfinementOptions{},
+ snippet: "snippet",
+ content: "default\nsnippet\n",
+}, {
+ opts: interfaces.ConfinementOptions{DevMode: true},
+ content: "@complain\ndefault\n",
+}, {
+ opts: interfaces.ConfinementOptions{DevMode: true},
+ snippet: "snippet",
+ content: "@complain\ndefault\nsnippet\n",
+}, {
+ opts: interfaces.ConfinementOptions{Classic: true},
+ snippet: "snippet",
+ content: "@unrestricted\ndefault\nsnippet\n",
+}, {
+ opts: interfaces.ConfinementOptions{Classic: true, JailMode: true},
+ snippet: "snippet",
+ content: "default\nsnippet\n",
+}}
+
+func (s *backendSuite) TestCombineSnippets(c *C) {
+ // NOTE: replace the real template with a shorter variant
+ restore := seccomp.MockTemplate([]byte("default\n"))
+ defer restore()
+ for _, scenario := range combineSnippetsScenarios {
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ if scenario.snippet == "" {
+ return nil, nil
+ }
+ return []byte(scenario.snippet), nil
+ }
+ snapInfo := s.InstallSnap(c, scenario.opts, ifacetest.SambaYamlV1, 0)
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.smbd")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ c.Check(string(data), Equals, scenario.content)
+ stat, err := os.Stat(profile)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode(), Equals, os.FileMode(0644))
+ s.RemoveSnap(c, snapInfo)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package seccomp
+
+// MockTemplate replaces seccomp template.
+//
+// NOTE: The real seccomp template is long. For testing it is convenient for
+// replace it with a shorter snippet.
+func MockTemplate(fakeTemplate []byte) (restore func()) {
+ orig := defaultTemplate
+ defaultTemplate = fakeTemplate
+ return func() { defaultTemplate = orig }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package seccomp_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package seccomp
+
+// defaultTemplate contains default seccomp template.
+//
+// It can be overridden for testing using MockTemplate().
+//
+// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/templates/ubuntu-core/16.04/default
+var defaultTemplate = []byte(`
+# Description: Allows access to app-specific directories and basic runtime
+# Usage: common
+#
+
+# Dangerous syscalls that we don't ever want to allow.
+# Note: may uncomment once ubuntu-core-launcher understands @deny rules and
+# if/when we conditionally deny these in the future.
+
+# kexec
+#@deny kexec_load
+
+# kernel modules
+#@deny create_module
+#@deny init_module
+#@deny finit_module
+#@deny delete_module
+
+# these have a history of vulnerabilities, are not widely used, and
+# open_by_handle_at has been used to break out of docker containers by brute
+# forcing the handle value: http://stealth.openwall.net/xSports/shocker.c
+#@deny name_to_handle_at
+#@deny open_by_handle_at
+
+# Explicitly deny ptrace since it can be abused to break out of the seccomp
+# sandbox
+#@deny ptrace
+
+# Explicitly deny capability mknod so apps can't create devices
+#@deny mknod
+#@deny mknodat
+
+# Explicitly deny (u)mount so apps can't change mounts in their namespace
+#@deny mount
+#@deny umount
+#@deny umount2
+
+# Explicitly deny kernel keyring access
+#@deny add_key
+#@deny keyctl
+#@deny request_key
+
+# end dangerous syscalls
+
+access
+faccessat
+
+alarm
+brk
+
+# ARM private syscalls
+breakpoint
+cacheflush
+set_tls
+usr26
+usr32
+
+capget
+# AppArmor mediates capabilities, so allow capset (useful for apps that for
+# example want to drop capabilities)
+capset
+
+chdir
+fchdir
+
+# We can't effectively block file perms due to open() with O_CREAT, so allow
+# chmod until we have syscall arg filtering (LP: #1446748)
+chmod
+fchmod
+fchmodat
+
+# snappy doesn't currently support per-app UID/GIDs so don't allow chown. To
+# properly support chown, we need to have syscall arg filtering (LP: #1446748)
+# and per-app UID/GIDs.
+#chown
+#chown32
+#fchown
+#fchown32
+#fchownat
+#lchown
+#lchown32
+
+clock_getres
+clock_gettime
+clock_nanosleep
+clone
+close
+creat
+dup
+dup2
+dup3
+epoll_create
+epoll_create1
+epoll_ctl
+epoll_ctl_old
+epoll_pwait
+epoll_wait
+epoll_wait_old
+eventfd
+eventfd2
+execve
+execveat
+_exit
+exit
+exit_group
+fallocate
+
+# requires CAP_SYS_ADMIN
+#fanotify_init
+#fanotify_mark
+
+fcntl
+fcntl64
+flock
+fork
+ftime
+futex
+get_mempolicy
+get_robust_list
+get_thread_area
+getcpu
+getcwd
+getdents
+getdents64
+getegid
+getegid32
+geteuid
+geteuid32
+getgid
+getgid32
+getgroups
+getgroups32
+getitimer
+getpgid
+getpgrp
+getpid
+getppid
+getpriority
+getrandom
+getresgid
+getresgid32
+getresuid
+getresuid32
+
+getrlimit
+ugetrlimit
+
+getrusage
+getsid
+gettid
+gettimeofday
+getuid
+getuid32
+
+getxattr
+fgetxattr
+lgetxattr
+
+inotify_add_watch
+inotify_init
+inotify_init1
+inotify_rm_watch
+
+# Needed by shell
+ioctl
+
+io_cancel
+io_destroy
+io_getevents
+io_setup
+io_submit
+ioprio_get
+# affects other processes, requires CAP_SYS_ADMIN. Potentially allow with
+# syscall filtering of (at least) IOPRIO_WHO_USER (LP: #1446748)
+#ioprio_set
+
+ipc
+kill
+link
+linkat
+
+listxattr
+llistxattr
+flistxattr
+
+lseek
+llseek
+_llseek
+lstat
+lstat64
+
+madvise
+fadvise64
+fadvise64_64
+arm_fadvise64_64
+
+mbind
+membarrier
+memfd_create
+mincore
+mkdir
+mkdirat
+mlock
+mlock2
+mlockall
+mmap
+mmap2
+modify_ldt
+mprotect
+
+# LP: #1448184 - these aren't currently mediated by AppArmor. Deny for now
+#mq_getsetattr
+#mq_notify
+#mq_open
+#mq_timedreceive
+#mq_timedsend
+#mq_unlink
+
+mremap
+msgctl
+msgget
+msgrcv
+msgsnd
+msync
+munlock
+munlockall
+munmap
+
+nanosleep
+
+# LP: #1446748 - deny until we have syscall arg filtering. Alternatively, set
+# RLIMIT_NICE hard limit for apps, launch them under an appropriate nice value
+# and allow this call
+#nice
+
+# LP: #1446748 - support syscall arg filtering for mode_t with O_CREAT
+open
+
+openat
+pause
+personality
+pipe
+pipe2
+poll
+ppoll
+
+# LP: #1446748 - support syscall arg filtering
+prctl
+arch_prctl
+
+read
+pread
+pread64
+preadv
+readv
+
+readahead
+readdir
+readlink
+readlinkat
+remap_file_pages
+
+removexattr
+fremovexattr
+lremovexattr
+
+rename
+renameat
+renameat2
+
+# The man page says this shouldn't be needed, but we've seen denials for it
+# in the wild
+restart_syscall
+
+rmdir
+rt_sigaction
+rt_sigpending
+rt_sigprocmask
+rt_sigqueueinfo
+rt_sigreturn
+rt_sigsuspend
+rt_sigtimedwait
+rt_tgsigqueueinfo
+sched_getaffinity
+sched_getattr
+sched_getparam
+sched_get_priority_max
+sched_get_priority_min
+sched_getscheduler
+sched_rr_get_interval
+# LP: #1446748 - when support syscall arg filtering, enforce pid_t is 0 so the
+# app may only change its own scheduler
+sched_setscheduler
+
+sched_yield
+
+# Allow configuring seccomp filter. This is ok because the kernel enforces that
+# the new filter is a subset of the current filter (ie, no widening
+# permissions)
+seccomp
+
+select
+_newselect
+pselect
+pselect6
+
+semctl
+semget
+semop
+semtimedop
+sendfile
+sendfile64
+
+# While we don't yet have seccomp arg filtering (LP: #1446748), we must allow
+# these because the launcher drops privileges after seccomp_load(). Eventually
+# we will only allow dropping to particular UIDs. For now, we mediate this with
+# AppArmor
+setgid
+setgid32
+setregid
+setregid32
+setresgid
+setresgid32
+setresuid
+setresuid32
+setreuid
+setreuid32
+setuid
+setuid32
+#setgroups
+#setgroups32
+
+# These break isolation but are common and can't be mediated at the seccomp
+# level with arg filtering
+setpgid
+setpgrp
+
+set_thread_area
+setitimer
+
+# apps don't have CAP_SYS_RESOURCE so these can't be abused to raise the hard
+# limits
+setrlimit
+prlimit64
+
+set_mempolicy
+set_robust_list
+setsid
+set_tid_address
+
+setxattr
+fsetxattr
+lsetxattr
+
+shmat
+shmctl
+shmdt
+shmget
+signal
+sigaction
+signalfd
+signalfd4
+sigaltstack
+sigpending
+sigprocmask
+sigreturn
+sigsuspend
+sigtimedwait
+sigwaitinfo
+
+# needed by ls -l
+socket
+connect
+
+# needed by snapctl
+getsockopt
+setsockopt
+getsockname
+getpeername
+
+# Per man page, on Linux this is limited to only AF_UNIX so it is ok to have
+# in the default template
+socketpair
+
+splice
+
+stat
+stat64
+fstat
+fstat64
+fstatat64
+lstat
+newfstatat
+oldfstat
+oldlstat
+oldstat
+
+statfs
+statfs64
+fstatfs
+fstatfs64
+statvfs
+fstatvfs
+ustat
+
+symlink
+symlinkat
+
+sync
+sync_file_range
+sync_file_range2
+arm_sync_file_range
+fdatasync
+fsync
+syncfs
+sysinfo
+syslog
+tee
+tgkill
+time
+timer_create
+timer_delete
+timer_getoverrun
+timer_gettime
+timer_settime
+timerfd
+timerfd_create
+timerfd_gettime
+timerfd_settime
+times
+tkill
+
+truncate
+truncate64
+ftruncate
+ftruncate64
+
+umask
+
+uname
+olduname
+oldolduname
+
+unlink
+unlinkat
+
+utime
+utimensat
+utimes
+futimesat
+
+vfork
+vmsplice
+wait4
+oldwait4
+waitpid
+waitid
+
+write
+writev
+pwrite
+pwrite64
+pwritev
+
+# FIXME: remove this after LP: #1446748 is implemented
+# This is an older interface and single entry point that can be used instead
+# of socket(), bind(), connect(), etc individually.
+socketcall
+`)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+type bySlotRef []SlotRef
+
+func (c bySlotRef) Len() int { return len(c) }
+func (c bySlotRef) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+func (c bySlotRef) Less(i, j int) bool {
+ if c[i].Snap != c[j].Snap {
+ return c[i].Snap < c[j].Snap
+ }
+ return c[i].Name < c[j].Name
+}
+
+type byPlugRef []PlugRef
+
+func (c byPlugRef) Len() int { return len(c) }
+func (c byPlugRef) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+func (c byPlugRef) Less(i, j int) bool {
+ if c[i].Snap != c[j].Snap {
+ return c[i].Snap < c[j].Snap
+ }
+ return c[i].Name < c[j].Name
+}
+
+type byPlugSnapAndName []*Plug
+
+func (c byPlugSnapAndName) Len() int { return len(c) }
+func (c byPlugSnapAndName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+func (c byPlugSnapAndName) Less(i, j int) bool {
+ if c[i].Snap.Name() != c[j].Snap.Name() {
+ return c[i].Snap.Name() < c[j].Snap.Name()
+ }
+ return c[i].Name < c[j].Name
+}
+
+type bySlotSnapAndName []*Slot
+
+func (c bySlotSnapAndName) Len() int { return len(c) }
+func (c bySlotSnapAndName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+func (c bySlotSnapAndName) Less(i, j int) bool {
+ if c[i].Snap.Name() != c[j].Snap.Name() {
+ return c[i].Snap.Name() < c[j].Snap.Name()
+ }
+ return c[i].Name < c[j].Name
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package interfaces
+
+import (
+ "sort"
+
+ . "gopkg.in/check.v1"
+)
+
+type SortingSuite struct{}
+
+var _ = Suite(&SortingSuite{})
+
+func (s *SortingSuite) TestSortBySlotRef(c *C) {
+ list := []SlotRef{
+ {
+ Snap: "snap-2",
+ Name: "name-2",
+ },
+ {
+ Snap: "snap-1",
+ Name: "name-2",
+ },
+ {
+ Snap: "snap-1",
+ Name: "name-1",
+ },
+ }
+ sort.Sort(bySlotRef(list))
+ c.Assert(list, DeepEquals, []SlotRef{
+ {
+ Snap: "snap-1",
+ Name: "name-1",
+ },
+ {
+ Snap: "snap-1",
+ Name: "name-2",
+ },
+ {
+ Snap: "snap-2",
+ Name: "name-2",
+ },
+ })
+}
+
+func (s *SortingSuite) TestSortByPlugRef(c *C) {
+ list := []PlugRef{
+ {
+ Snap: "snap-2",
+ Name: "name-2",
+ },
+ {
+ Snap: "snap-1",
+ Name: "name-2",
+ },
+ {
+ Snap: "snap-1",
+ Name: "name-1",
+ },
+ }
+ sort.Sort(byPlugRef(list))
+ c.Assert(list, DeepEquals, []PlugRef{
+ {
+ Snap: "snap-1",
+ Name: "name-1",
+ },
+ {
+ Snap: "snap-1",
+ Name: "name-2",
+ },
+ {
+ Snap: "snap-2",
+ Name: "name-2",
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package systemd implements integration between snappy interfaces and
+// arbitrary systemd units that may be required for "oneshot" style tasks.
+package systemd
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ sysd "github.com/snapcore/snapd/systemd"
+)
+
+// FIXME: This backend unmarshals snippets as JSON. This is a hack that needs to be fixed
+// by making the interface methods get a typed backend object to talk to instead.
+
+// Backend is responsible for maintaining apparmor profiles for ubuntu-core-launcher.
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "systemd"
+}
+
+func disableRemovedServices(systemd sysd.Systemd, dir, glob string, content map[string]*osutil.FileState) error {
+ paths, err := filepath.Glob(filepath.Join(dir, glob))
+ if err != nil {
+ return err
+ }
+ for _, path := range paths {
+ service := filepath.Base(path)
+ if content[service] == nil {
+ if err := systemd.Disable(service); err != nil {
+ logger.Noticef("cannot disable service %q: %s", service, err)
+ }
+ if err := systemd.Stop(service, 5*time.Second); err != nil {
+ logger.Noticef("cannot stop service %q: %s", service, err)
+ }
+ }
+ }
+ return nil
+}
+
+func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ rawSnippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecuritySystemd)
+ if err != nil {
+ return fmt.Errorf("cannot obtain systemd security snippets for snap %q: %s", snapName, err)
+ }
+ snippets, err := unmarshalRawSnippetMap(rawSnippets)
+ if err != nil {
+ return fmt.Errorf("cannot unmarshal systemd snippets for snap %q: %s", snapName, err)
+ }
+ snippet, err := mergeSnippetMap(snippets)
+ if err != nil {
+ return fmt.Errorf("cannot merge systemd snippets for snap %q: %s", snapName, err)
+ }
+ content, err := renderSnippet(snippet)
+ if err != nil {
+ return fmt.Errorf("cannot render systemd snippets for snap %q: %s", snapName, err)
+ }
+ dir := dirs.SnapServicesDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for systemd services %q: %s", dir, err)
+ }
+ glob := interfaces.InterfaceServiceName(snapName, "*")
+
+ systemd := sysd.New(dirs.GlobalRootDir, &dummyReporter{})
+ // We need to be carefully here and stop all removed service units before
+ // we remove their files as otherwise systemd is not able to disable/stop
+ // them anymore.
+ if err := disableRemovedServices(systemd, dir, glob, content); err != nil {
+ logger.Noticef("cannot stop removed services: %s", err)
+ }
+ changed, removed, errEnsure := osutil.EnsureDirState(dir, glob, content)
+ // Reload systemd whenever something is added or removed
+ if len(changed) > 0 || len(removed) > 0 {
+ err := systemd.DaemonReload()
+ if err != nil {
+ logger.Noticef("cannot reload systemd state: %s", err)
+ }
+ }
+ // Ensure the service is running right now and on reboots
+ for _, service := range changed {
+ if err := systemd.Enable(service); err != nil {
+ logger.Noticef("cannot enable service %q: %s", service, err)
+ }
+ // If we have a new service here which isn't started yet the restart
+ // operation will start it.
+ if err := systemd.Restart(service, 10*time.Second); err != nil {
+ logger.Noticef("cannot restart service %q: %s", service, err)
+ }
+ }
+ return errEnsure
+}
+
+func (b *Backend) Remove(snapName string) error {
+ systemd := sysd.New(dirs.GlobalRootDir, &dummyReporter{})
+ // Remove all the files matching snap glob
+ glob := interfaces.InterfaceServiceName(snapName, "*")
+ _, removed, errEnsure := osutil.EnsureDirState(dirs.SnapServicesDir, glob, nil)
+ for _, service := range removed {
+ if err := systemd.Disable(service); err != nil {
+ logger.Noticef("cannot disable service %q: %s", service, err)
+ }
+ if err := systemd.Stop(service, 5*time.Second); err != nil {
+ logger.Noticef("cannot stop service %q: %s", service, err)
+ }
+ }
+ // Reload systemd whenever something is removed
+ if len(removed) > 0 {
+ err := systemd.DaemonReload()
+ if err != nil {
+ logger.Noticef("cannot reload systemd state: %s", err)
+ }
+ }
+ return errEnsure
+}
+
+func unmarshalRawSnippetMap(rawSnippetMap map[string][][]byte) (map[string][]*Snippet, error) {
+ richSnippetMap := make(map[string][]*Snippet)
+ for tag, rawSnippets := range rawSnippetMap {
+ for _, rawSnippet := range rawSnippets {
+ richSnippet := &Snippet{}
+ err := json.Unmarshal(rawSnippet, &richSnippet)
+ if err != nil {
+ return nil, err
+ }
+ richSnippetMap[tag] = append(richSnippetMap[tag], richSnippet)
+ }
+ }
+ return richSnippetMap, nil
+}
+
+// Flatten, deduplicate and check for conflicts in the services in the given snippet map
+func mergeSnippetMap(snippetMap map[string][]*Snippet) (*Snippet, error) {
+ services := make(map[string]Service)
+ for _, snippets := range snippetMap {
+ for _, snippet := range snippets {
+ for name, service := range snippet.Services {
+ if old, present := services[name]; present {
+ if old != service {
+ return nil, fmt.Errorf("interface require conflicting system needs")
+ }
+ } else {
+ services[name] = service
+ }
+ }
+ }
+ }
+ return &Snippet{Services: services}, nil
+}
+
+func renderSnippet(snippet *Snippet) (map[string]*osutil.FileState, error) {
+ content := make(map[string]*osutil.FileState)
+ for name, service := range snippet.Services {
+ content[name] = &osutil.FileState{
+ Content: []byte(service.String()),
+ Mode: 0644,
+ }
+ }
+ return content, nil
+}
+
+type dummyReporter struct{}
+
+func (dr *dummyReporter) Notify(msg string) {
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd_test
+
+import (
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/interfaces/systemd"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+
+ sysd "github.com/snapcore/snapd/systemd"
+)
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+ systemctlCmd *testutil.MockCmd
+}
+
+var _ = Suite(&backendSuite{})
+
+var testedConfinementOpts = []interfaces.ConfinementOptions{
+ {},
+ {DevMode: true},
+ {JailMode: true},
+ {Classic: true},
+}
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.BackendSuite.SetUpTest(c)
+ s.Backend = &systemd.Backend{}
+ s.systemctlCmd = testutil.MockCommand(c, "systemctl", "echo ActiveState=inactive")
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.systemctlCmd.Restore()
+ s.BackendSuite.TearDownTest(c)
+}
+
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "systemd")
+}
+
+func (s *backendSuite) TestUnmarshalRawSnippetMap(c *C) {
+ rawSnippetMap := map[string][][]byte{
+ "security-tag": {
+ []byte(`{"services": {"foo.service": {"exec-start": "/bin/true"}}}`),
+ []byte(`{"services": {"bar.service": {"exec-start": "/bin/false"}}}`),
+ },
+ }
+ richSnippetMap, err := systemd.UnmarshalRawSnippetMap(rawSnippetMap)
+ c.Assert(err, IsNil)
+ c.Assert(richSnippetMap, DeepEquals, map[string][]*systemd.Snippet{
+ "security-tag": {
+ {
+ Services: map[string]systemd.Service{
+ "foo.service": {ExecStart: "/bin/true"},
+ },
+ },
+ {
+ Services: map[string]systemd.Service{
+ "bar.service": {ExecStart: "/bin/false"},
+ },
+ },
+ },
+ })
+}
+
+func (s *backendSuite) TestMergeSnippetMapOK(c *C) {
+ snippetMap := map[string][]*systemd.Snippet{
+ "security-tag": {
+ {
+ Services: map[string]systemd.Service{
+ "foo.service": {ExecStart: "/bin/true"},
+ },
+ },
+ },
+ "another-tag": {
+ {
+ Services: map[string]systemd.Service{
+ "bar.service": {ExecStart: "/bin/false"},
+ },
+ },
+ },
+ }
+ snippet, err := systemd.MergeSnippetMap(snippetMap)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, DeepEquals, &systemd.Snippet{
+ Services: map[string]systemd.Service{
+ "foo.service": {ExecStart: "/bin/true"},
+ "bar.service": {ExecStart: "/bin/false"},
+ },
+ })
+}
+
+func (s *backendSuite) TestMergeSnippetMapClashing(c *C) {
+ snippetMap := map[string][]*systemd.Snippet{
+ "security-tag": {
+ {
+ Services: map[string]systemd.Service{
+ "foo.service": {ExecStart: "/bin/true"},
+ },
+ },
+ },
+ "another-tag": {
+ {
+ Services: map[string]systemd.Service{
+ "foo.service": {ExecStart: "/bin/evil"},
+ },
+ },
+ },
+ }
+ snippet, err := systemd.MergeSnippetMap(snippetMap)
+ c.Assert(err, ErrorMatches, `interface require conflicting system needs`)
+ c.Assert(snippet, IsNil)
+}
+
+func (s *backendSuite) TestRenderSnippet(c *C) {
+ snippet := &systemd.Snippet{
+ Services: map[string]systemd.Service{
+ "foo.service": {ExecStart: "/bin/true"},
+ },
+ }
+ content, err := systemd.RenderSnippet(snippet)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, map[string]*osutil.FileState{
+ "foo.service": {
+ Content: []byte("[Service]\nExecStart=/bin/true\n\n[Install]\nWantedBy=multi-user.target\n"),
+ Mode: 0644,
+ },
+ })
+}
+
+func (s *backendSuite) TestInstallingSnapWritesStartsServices(c *C) {
+ prevctlCmd := sysd.SystemctlCmd
+ var sysdLog [][]string
+ sysd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ sysdLog = append(sysdLog, cmd)
+ if cmd[0] == "show" {
+ return []byte("ActiveState=inactive\n"), nil
+ }
+ return []byte{}, nil
+ }
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(`{"services": {"snap.samba.interface.foo.service": {"exec-start": "/bin/true"}}}`), nil
+ }
+ s.InstallSnap(c, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 1)
+ service := filepath.Join(dirs.SnapServicesDir, "snap.samba.interface.foo.service")
+ // the service file was created
+ _, err := os.Stat(service)
+ c.Check(err, IsNil)
+ // the service was also started (whee)
+ c.Check(sysdLog, DeepEquals, [][]string{
+ {"daemon-reload"},
+ {"--root", dirs.GlobalRootDir, "enable", "snap.samba.interface.foo.service"},
+ {"stop", "snap.samba.interface.foo.service"},
+ {"show", "--property=ActiveState", "snap.samba.interface.foo.service"},
+ {"start", "snap.samba.interface.foo.service"},
+ })
+ sysd.SystemctlCmd = prevctlCmd
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesAndStopsServices(c *C) {
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(`{"services": {"snap.samba.interface.foo.service": {"exec-start": "/bin/true"}}}`), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 1)
+ s.systemctlCmd.ForgetCalls()
+ s.RemoveSnap(c, snapInfo)
+ service := filepath.Join(dirs.SnapServicesDir, "snap.samba.interface.foo.service")
+ // the service file was removed
+ _, err := os.Stat(service)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // the service was stopped
+ c.Check(s.systemctlCmd.Calls(), DeepEquals, [][]string{
+ {"systemctl", "--root", dirs.GlobalRootDir, "disable", "snap.samba.interface.foo.service"},
+ {"systemctl", "stop", "snap.samba.interface.foo.service"},
+ {"systemctl", "show", "--property=ActiveState", "snap.samba.interface.foo.service"},
+ {"systemctl", "daemon-reload"},
+ })
+ }
+}
+
+func (s *backendSuite) TestSettingUpSecurityWithFewerServices(c *C) {
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(`{"services": {"snap.samba.interface.foo.service": {"exec-start": "/bin/true"}, "snap.samba.interface.bar.service": {"exec-start": "/bin/false"}}}`), nil
+ }
+ snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 1)
+ s.systemctlCmd.ForgetCalls()
+ serviceFoo := filepath.Join(dirs.SnapServicesDir, "snap.samba.interface.foo.service")
+ serviceBar := filepath.Join(dirs.SnapServicesDir, "snap.samba.interface.bar.service")
+ // the services were created
+ _, err := os.Stat(serviceFoo)
+ c.Check(err, IsNil)
+ _, err = os.Stat(serviceBar)
+ c.Check(err, IsNil)
+
+ // Change what the interface returns to simulate some useful change
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte(`{"services": {"snap.samba.interface.foo.service": {"exec-start": "/bin/true"}}}`), nil
+ }
+ // Update over to the same snap to regenerate security
+ s.UpdateSnap(c, snapInfo, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 0)
+ // The bar service should have been stopped
+ c.Check(s.systemctlCmd.Calls(), DeepEquals, [][]string{
+ {"systemctl", "--root", dirs.GlobalRootDir, "disable", "snap.samba.interface.bar.service"},
+ {"systemctl", "stop", "snap.samba.interface.bar.service"},
+ {"systemctl", "show", "--property=ActiveState", "snap.samba.interface.bar.service"},
+ {"systemctl", "daemon-reload"},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd
+
+var (
+ UnmarshalRawSnippetMap = unmarshalRawSnippetMap
+ MergeSnippetMap = mergeSnippetMap
+ RenderSnippet = renderSnippet
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd
+
+import (
+ "bytes"
+ "fmt"
+)
+
+// Snippet describes systemd services that interface wishes to create.
+// Identical services from all snippets are combined and ignored.
+type Snippet struct {
+ Services map[string]Service `json:"services,omitempty"`
+}
+
+// Service describes a single systemd service file
+type Service struct {
+ Description string `json:"description,omitempty"`
+ Type string `json:"type"`
+ RemainAfterExit bool `json:"remain-after-exit,omitempty"`
+ ExecStart string `json:"exec-start,omitempty"`
+ ExecStop string `json:"exec-stop,omitempty"`
+}
+
+func (s *Service) String() string {
+ var buf bytes.Buffer
+ if s.Description != "" {
+ buf.WriteString("[Unit]\n")
+ fmt.Fprintf(&buf, "Description=%s\n\n", s.Description)
+ }
+ buf.WriteString("[Service]\n")
+ if s.Type != "" {
+ fmt.Fprintf(&buf, "Type=%s\n", s.Type)
+ }
+ // "no" is the default in systemd so we don't neet to write it
+ if s.RemainAfterExit {
+ buf.WriteString("RemainAfterExit=yes\n")
+ }
+ if s.ExecStart != "" {
+ fmt.Fprintf(&buf, "ExecStart=%s\n", s.ExecStart)
+ }
+ if s.ExecStop != "" {
+ fmt.Fprintf(&buf, "ExecStop=%s\n", s.ExecStop)
+ }
+ fmt.Fprintf(&buf, "\n[Install]\nWantedBy=multi-user.target\n")
+ return buf.String()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces/systemd"
+)
+
+type snippetSuite struct{}
+
+var _ = Suite(&snippetSuite{})
+
+func (s *snippetSuite) TestString(c *C) {
+ service1 := systemd.Service{ExecStart: "/bin/true"}
+ c.Assert(service1.String(), Equals, "[Service]\nExecStart=/bin/true\n\n[Install]\nWantedBy=multi-user.target\n")
+ service2 := systemd.Service{Type: "oneshot"}
+ c.Assert(service2.String(), Equals, "[Service]\nType=oneshot\n\n[Install]\nWantedBy=multi-user.target\n")
+ service3 := systemd.Service{RemainAfterExit: true}
+ c.Assert(service3.String(), Equals, "[Service]\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target\n")
+ service4 := systemd.Service{RemainAfterExit: false}
+ c.Assert(service4.String(), Equals, "[Service]\n\n[Install]\nWantedBy=multi-user.target\n")
+ service5 := systemd.Service{ExecStop: "/bin/true"}
+ c.Assert(service5.String(), Equals, "[Service]\nExecStop=/bin/true\n\n[Install]\nWantedBy=multi-user.target\n")
+ service6 := systemd.Service{Description: "ohai"}
+ c.Assert(service6.String(), Equals, "[Unit]\nDescription=ohai\n\n[Service]\n\n[Install]\nWantedBy=multi-user.target\n")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package udev implements integration between snappy, udev and
+// ubuntu-core-laucher around tagging character and block devices so that they
+// can be accessed by applications.
+//
+// TODO: Document this better
+package udev
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining udev rules.
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "udev"
+}
+
+// snapRulesFileName returns the path of the snap udev rules file.
+func snapRulesFilePath(snapName string) string {
+ rulesFileName := fmt.Sprintf("70-%s.rules", snap.SecurityTag(snapName))
+ return filepath.Join(dirs.SnapUdevRulesDir, rulesFileName)
+}
+
+// Setup creates udev rules specific to a given snap.
+// If any of the rules are changed or removed then udev database is reloaded.
+//
+// Udev has no concept of a complain mode so confinment options are ignored.
+//
+// If the method fails it should be re-tried (with a sensible strategy) by the caller.
+func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ snapName := snapInfo.Name()
+ snippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecurityUDev)
+ if err != nil {
+ return fmt.Errorf("cannot obtain udev security snippets for snap %q: %s", snapName, err)
+ }
+ content, err := b.combineSnippets(snapInfo, snippets)
+ if err != nil {
+ return fmt.Errorf("cannot obtain expected udev rules for snap %q: %s", snapName, err)
+ }
+ dir := dirs.SnapUdevRulesDir
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("cannot create directory for udev rules %q: %s", dir, err)
+ }
+
+ rulesFilePath := snapRulesFilePath(snapInfo.Name())
+
+ if len(content) == 0 {
+ // Make sure that the rules file gets removed when we don't have any
+ // content and exists.
+ err = os.Remove(rulesFilePath)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ } else if err == nil {
+ return ReloadRules()
+ }
+ return nil
+ }
+
+ var buffer bytes.Buffer
+ buffer.WriteString("# This file is automatically generated.\n")
+ for _, snippet := range content {
+ buffer.Write(snippet)
+ buffer.WriteByte('\n')
+ }
+
+ rulesFileState := &osutil.FileState{
+ Content: buffer.Bytes(),
+ Mode: 0644,
+ }
+
+ // EnsureFileState will make sure the file will be only updated when its content
+ // has changed and will otherwise return an error which prevents us from reloading
+ // udev rules when not needed.
+ err = osutil.EnsureFileState(rulesFilePath, rulesFileState)
+ if err == osutil.ErrSameState {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ return ReloadRules()
+}
+
+// Remove removes udev rules specific to a given snap.
+// If any of the rules are removed then udev database is reloaded.
+//
+// This method should be called after removing a snap.
+//
+// If the method fails it should be re-tried (with a sensible strategy) by the caller.
+func (b *Backend) Remove(snapName string) error {
+ rulesFilePath := snapRulesFilePath(snapName)
+ err := os.Remove(rulesFilePath)
+ if os.IsNotExist(err) {
+ // If file doesn't exist we avoid reloading the udev rules when we return here
+ return nil
+ } else if err != nil {
+ return err
+ }
+ return ReloadRules()
+}
+
+// combineSnippets combines security snippets collected from all the interfaces
+// affecting a given snap into a content map applicable to EnsureDirState.
+func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (result [][]byte, err error) {
+ var snapSnippets = make(map[string][]byte)
+
+ // We put all snippets from apps and hooks in the following part in a
+ // map to reach a deduplicated set of snippets we can then write out
+ // in a per snap udev rules file.
+
+ for _, appInfo := range snapInfo.Apps {
+ securityTag := appInfo.SecurityTag()
+ appSnippets := snippets[securityTag]
+ if len(appSnippets) == 0 {
+ continue
+ }
+
+ for _, snippet := range appSnippets {
+ snapSnippets[string(snippet)] = snippet
+ }
+ }
+
+ for _, hookInfo := range snapInfo.Hooks {
+ securityTag := hookInfo.SecurityTag()
+ hookSnippets := snippets[securityTag]
+ if len(hookSnippets) == 0 {
+ continue
+ }
+
+ for _, snippet := range hookSnippets {
+ snapSnippets[string(snippet)] = snippet
+ }
+ }
+
+ nonePrefix := snap.NoneSecurityTag(snapInfo.Name(), "")
+ for securityTag, slotSnippets := range snippets {
+ if !strings.HasPrefix(securityTag, nonePrefix) {
+ continue
+ }
+
+ for _, snippet := range slotSnippets {
+ snapSnippets[string(snippet)] = snippet
+ }
+ }
+
+ var combinedSnippets [][]byte
+ for _, snippet := range snapSnippets {
+ combinedSnippets = append(combinedSnippets, snippet)
+ }
+
+ return combinedSnippets, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package udev_test
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/interfaces/udev"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type backendSuite struct {
+ ifacetest.BackendSuite
+
+ udevadmCmd *testutil.MockCmd
+}
+
+var _ = Suite(&backendSuite{})
+
+var testedConfinementOpts = []interfaces.ConfinementOptions{
+ {},
+ {DevMode: true},
+ {JailMode: true},
+ {Classic: true},
+}
+
+func createSnippetForApps(apps map[string]*snap.AppInfo) []byte {
+ var buffer bytes.Buffer
+ for appName := range apps {
+ buffer.WriteString(appName)
+ }
+ return buffer.Bytes()
+}
+
+func (s *backendSuite) SetUpTest(c *C) {
+ s.Backend = &udev.Backend{}
+
+ s.BackendSuite.SetUpTest(c)
+
+ // Mock away any real udev interaction
+ s.udevadmCmd = testutil.MockCommand(c, "udevadm", "")
+ // Prepare a directory for udev rules
+ // NOTE: Normally this is a part of the OS snap.
+ err := os.MkdirAll(dirs.SnapUdevRulesDir, 0700)
+ c.Assert(err, IsNil)
+}
+
+func (s *backendSuite) TearDownTest(c *C) {
+ s.udevadmCmd.Restore()
+
+ s.BackendSuite.TearDownTest(c)
+}
+
+// Tests for Setup() and Remove()
+func (s *backendSuite) TestName(c *C) {
+ c.Check(s.Backend.Name(), Equals, "udev")
+}
+
+func (s *backendSuite) TestInstallingSnapWritesAndLoadsRules(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ s.udevadmCmd.ForgetCalls()
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ // file called "70-snap.sambda.rules" was created
+ _, err := os.Stat(fname)
+ c.Check(err, IsNil)
+ // udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestInstallingSnapWithHookWritesAndLoadsRules(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(slot *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ s.udevadmCmd.ForgetCalls()
+ snapInfo := s.InstallSnap(c, opts, ifacetest.HookYaml, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules")
+
+ // Verify that "70-snap.foo.rules" was created.
+ _, err := os.Stat(fname)
+ c.Check(err, IsNil)
+
+ // Verify that udevadm was used to reload rules and re-run triggers.
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestSecurityIsStable(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.udevadmCmd.ForgetCalls()
+ err := s.Backend.Setup(snapInfo, opts, s.Repo)
+ c.Assert(err, IsNil)
+ // rules are not re-loaded when nothing changes
+ c.Check(s.udevadmCmd.Calls(), HasLen, 0)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestRemovingSnapRemovesAndReloadsRules(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.udevadmCmd.ForgetCalls()
+ s.RemoveSnap(c, snapInfo)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ // file called "70-snap.sambda.rules" was removed
+ _, err := os.Stat(fname)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return createSnippetForApps(slot.Apps), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.udevadmCmd.ForgetCalls()
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ // file called "70-snap.sambda.rules" was created
+ _, err := os.Stat(fname)
+ c.Check(err, IsNil)
+ // udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return createSnippetForApps(slot.Apps), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(slot *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.udevadmCmd.ForgetCalls()
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlWithHook, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+
+ // Verify that "70-snap.samba.rules" was created
+ _, err := os.Stat(fname)
+ c.Check(err, IsNil)
+
+ // Verify that udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return createSnippetForApps(slot.Apps), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1WithNmbd, 0)
+ s.udevadmCmd.ForgetCalls()
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ // file called "70-snap.sambda.rules" still exists
+ _, err := os.Stat(fname)
+ c.Check(err, IsNil)
+ // udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithFewerHooks(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return createSnippetForApps(slot.Apps), nil
+ }
+ s.Iface.PermanentPlugSnippetCallback = func(slot *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlWithHook, 0)
+ s.udevadmCmd.ForgetCalls()
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ // file called "70-snap.sambda.rules" still exists
+ _, err := os.Stat(fname)
+ c.Check(err, IsNil)
+ // Verify that udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestCombineSnippetsWithActualSnippets(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ data, err := ioutil.ReadFile(fname)
+ c.Assert(err, IsNil)
+ c.Check(string(data), Equals, "# This file is automatically generated.\ndummy\n")
+ stat, err := os.Stat(fname)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode(), Equals, os.FileMode(0644))
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWhenPlugNoApps(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.PlugNoAppsYaml, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules")
+ data, err := ioutil.ReadFile(fname)
+ c.Assert(err, IsNil)
+ c.Check(string(data), Equals, "# This file is automatically generated.\ndummy\n")
+ stat, err := os.Stat(fname)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode(), Equals, os.FileMode(0644))
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWhenSlotNoApps(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SlotNoAppsYaml, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules")
+ data, err := ioutil.ReadFile(fname)
+ c.Assert(err, IsNil)
+ c.Check(string(data), Equals, "# This file is automatically generated.\ndummy\n")
+ stat, err := os.Stat(fname)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode(), Equals, os.FileMode(0644))
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestCombineSnippetsWithoutAnySnippets(c *C) {
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ _, err := os.Stat(fname)
+ // Without any snippets, there the .rules file is not created.
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapToOneWithoutSlots(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1, 0)
+ s.udevadmCmd.ForgetCalls()
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1NoSlot, 0)
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ // file called "70-snap.sambda.rules" was removed
+ _, err := os.Stat(fname)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // Verify that udevadm was used to reload rules and re-run triggers
+ c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+ s.RemoveSnap(c, snapInfo)
+ }
+}
+
+func (s *backendSuite) TestUpdatingSnapWithoutSlotsToOneWithoutSlots(c *C) {
+ // NOTE: Hand out a permanent snippet so that .rules file is generated.
+ s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return []byte("dummy"), nil
+ }
+ for _, opts := range testedConfinementOpts {
+ snapInfo := s.InstallSnap(c, opts, ifacetest.SambaYamlV1NoSlot, 0)
+ // file called "70-snap.sambda.rules" does not exist
+ fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
+ _, err := os.Stat(fname)
+ c.Check(os.IsNotExist(err), Equals, true)
+ s.udevadmCmd.ForgetCalls()
+
+ snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbdNoSlot, 0)
+ // file called "70-snap.sambda.rules" still does not exist
+ _, err = os.Stat(fname)
+ c.Check(os.IsNotExist(err), Equals, true)
+ // Verify that udevadm was used to reload rules and re-run triggers
+ c.Check(len(s.udevadmCmd.Calls()), Equals, 0)
+ s.RemoveSnap(c, snapInfo)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package udev
+
+import (
+ "fmt"
+ "os/exec"
+)
+
+// ReloadRules runs two commands that reload udev rule database.
+//
+// The commands are: udevadm control --reload-rules
+// udevadm trigger
+func ReloadRules() error {
+ output, err := exec.Command("udevadm", "control", "--reload-rules").CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot reload udev rules: %s\nudev output:\n%s", err, string(output))
+ }
+ output, err = exec.Command("udevadm", "trigger").CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot run udev triggers: %s\nudev output:\n%s", err, string(output))
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package udev_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces/udev"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type uDevSuite struct{}
+
+var _ = Suite(&uDevSuite{})
+
+// Tests for ReloadRules()
+
+func (s *uDevSuite) TestReloadUDevRulesRunsUDevAdm(c *C) {
+ cmd := testutil.MockCommand(c, "udevadm", "")
+ defer cmd.Restore()
+ err := udev.ReloadRules()
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+}
+
+func (s *uDevSuite) TestReloadUDevRulesReportsErrorsFromReloadRules(c *C) {
+ cmd := testutil.MockCommand(c, "udevadm", `
+if [ "$1" = "control" ]; then
+ echo "failure 1"
+ exit 1
+fi
+ `)
+ defer cmd.Restore()
+ err := udev.ReloadRules()
+ c.Assert(err.Error(), Equals, ""+
+ "cannot reload udev rules: exit status 1\n"+
+ "udev output:\n"+
+ "failure 1\n")
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ })
+}
+
+func (s *uDevSuite) TestReloadUDevRulesReportsErrorsFromTrigger(c *C) {
+ cmd := testutil.MockCommand(c, "udevadm", `
+if [ "$1" = "trigger" ]; then
+ echo "failure 2"
+ exit 2
+fi
+ `)
+ defer cmd.Restore()
+ err := udev.ReloadRules()
+ c.Assert(err.Error(), Equals, ""+
+ "cannot run udev triggers: exit status 2\n"+
+ "udev output:\n"+
+ "failure 2\n")
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {"udevadm", "control", "--reload-rules"},
+ {"udevadm", "trigger"},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package logger
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "log/syslog"
+ "os"
+ "sync"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+// A Logger is a fairly minimal logging tool.
+type Logger interface {
+ // Notice is for messages that the user should see
+ Notice(msg string)
+ // Debug is for messages that the user should be able to find if they're debugging something
+ Debug(msg string)
+}
+
+const (
+ // DefaultFlags are passed to the default console log.Logger
+ DefaultFlags = log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile
+ // SyslogFlags are passed to the default syslog log.Logger
+ SyslogFlags = log.Lshortfile
+ // SyslogPriority for the default syslog log.Logger
+ SyslogPriority = syslog.LOG_DEBUG | syslog.LOG_USER
+)
+
+type nullLogger struct{}
+
+func (nullLogger) Notice(string) {}
+func (nullLogger) Debug(string) {}
+
+// NullLogger is a logger that does nothing
+var NullLogger = nullLogger{}
+
+var (
+ logger Logger = NullLogger
+ lock sync.Mutex
+)
+
+// Panicf notifies the user and then panics
+func Panicf(format string, v ...interface{}) {
+ msg := fmt.Sprintf(format, v...)
+
+ lock.Lock()
+ defer lock.Unlock()
+
+ logger.Notice("PANIC " + msg)
+ panic(msg)
+}
+
+// Noticef notifies the user of something
+func Noticef(format string, v ...interface{}) {
+ msg := fmt.Sprintf(format, v...)
+
+ lock.Lock()
+ defer lock.Unlock()
+
+ logger.Notice(msg)
+}
+
+// Debugf records something in the debug log
+func Debugf(format string, v ...interface{}) {
+ msg := fmt.Sprintf(format, v...)
+
+ lock.Lock()
+ defer lock.Unlock()
+
+ logger.Debug(msg)
+}
+
+// SetLogger sets the global logger to the given one
+func SetLogger(l Logger) {
+ lock.Lock()
+ defer lock.Unlock()
+
+ logger = l
+}
+
+// ConsoleLog sends Notices to a log.Logger and Debugs to syslog
+type ConsoleLog struct {
+ log *log.Logger
+ sys *log.Logger
+}
+
+// Debug sends the msg to syslog
+func (l *ConsoleLog) Debug(msg string) {
+ s := "DEBUG: " + msg
+ l.sys.Output(3, s)
+
+ if osutil.GetenvBool("SNAPD_DEBUG") {
+ l.log.Output(3, s)
+ }
+}
+
+// Notice alerts the user about something, as well as putting it syslog
+func (l *ConsoleLog) Notice(msg string) {
+ l.sys.Output(3, msg)
+ l.log.Output(3, msg)
+}
+
+// variable to allow mocking the syslog.NewLogger call in the tests
+var newSyslog = newSyslogImpl
+
+func newSyslogImpl() (*log.Logger, error) {
+ return syslog.NewLogger(SyslogPriority, SyslogFlags)
+}
+
+// NewConsoleLog creates a ConsoleLog with a log.Logger using the given
+// io.Writer and flag, and a syslog.Writer.
+func NewConsoleLog(w io.Writer, flag int) (*ConsoleLog, error) {
+ clog := log.New(w, "", flag)
+
+ sys, err := newSyslog()
+ if err != nil {
+ clog.Output(3, "WARNING: cannot create syslog logger")
+ sys = log.New(ioutil.Discard, "", flag)
+ }
+
+ return &ConsoleLog{
+ log: clog,
+ sys: sys,
+ }, nil
+}
+
+// SimpleSetup creates the default (console) logger
+func SimpleSetup() error {
+ l, err := NewConsoleLog(os.Stderr, DefaultFlags)
+ if err != nil {
+ return err
+ }
+ SetLogger(l)
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package logger
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "os"
+ "testing"
+
+ "github.com/snapcore/snapd/testutil"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&LogSuite{})
+
+type LogSuite struct {
+ sysbuf *bytes.Buffer
+}
+
+func (s *LogSuite) SetUpTest(c *C) {
+ c.Assert(logger, Equals, NullLogger)
+
+ // we do not want to pollute syslog in our tests (and sbuild
+ // will also not let us do that)
+ newSyslog = func() (*log.Logger, error) {
+ s.sysbuf = bytes.NewBuffer(nil)
+ return log.New(s.sysbuf, "", SyslogFlags), nil
+ }
+}
+
+func (s *LogSuite) TearDownTest(c *C) {
+ SetLogger(NullLogger)
+ newSyslog = newSyslogImpl
+}
+
+func (s *LogSuite) TestDefault(c *C) {
+ if logger != nil {
+ SetLogger(nil)
+ }
+ c.Check(logger, IsNil)
+
+ err := SimpleSetup()
+ c.Check(err, IsNil)
+ c.Check(logger, NotNil)
+ SetLogger(nil)
+}
+
+func (s *LogSuite) TestNew(c *C) {
+ var buf bytes.Buffer
+ l, err := NewConsoleLog(&buf, DefaultFlags)
+ c.Assert(err, IsNil)
+ c.Assert(l, NotNil)
+ c.Check(l.sys, NotNil)
+ c.Check(l.log, NotNil)
+}
+
+func (s *LogSuite) TestDebugf(c *C) {
+ var logbuf bytes.Buffer
+ l, err := NewConsoleLog(&logbuf, DefaultFlags)
+ c.Assert(err, IsNil)
+
+ SetLogger(l)
+
+ Debugf("xyzzy")
+ c.Check(s.sysbuf.String(), Matches, `(?m).*logger_test\.go:\d+: DEBUG: xyzzy`)
+ c.Check(logbuf.String(), Equals, "")
+}
+
+func (s *LogSuite) TestDebugfEnv(c *C) {
+ var logbuf bytes.Buffer
+ l, err := NewConsoleLog(&logbuf, DefaultFlags)
+ c.Assert(err, IsNil)
+
+ SetLogger(l)
+
+ os.Setenv("SNAPD_DEBUG", "1")
+ defer os.Unsetenv("SNAPD_DEBUG")
+
+ Debugf("xyzzy")
+ c.Check(s.sysbuf.String(), Matches, `(?m).*logger_test\.go:\d+: DEBUG: xyzzy`)
+ c.Check(logbuf.String(), testutil.Contains, `DEBUG: xyzzy`)
+}
+
+func (s *LogSuite) TestNoticef(c *C) {
+ var logbuf bytes.Buffer
+ l, err := NewConsoleLog(&logbuf, DefaultFlags)
+ c.Assert(err, IsNil)
+
+ SetLogger(l)
+
+ Noticef("xyzzy")
+ c.Check(s.sysbuf.String(), Matches, `(?m).*logger_test\.go:\d+: xyzzy`)
+ c.Check(logbuf.String(), Matches, `(?m).*logger_test\.go:\d+: xyzzy`)
+}
+
+func (s *LogSuite) TestPanicf(c *C) {
+ var logbuf bytes.Buffer
+ l, err := NewConsoleLog(&logbuf, DefaultFlags)
+ c.Assert(err, IsNil)
+
+ SetLogger(l)
+
+ c.Check(func() { Panicf("xyzzy") }, Panics, "xyzzy")
+ c.Check(s.sysbuf.String(), Matches, `(?m).*logger_test\.go:\d+: PANIC xyzzy`)
+ c.Check(logbuf.String(), Matches, `(?m).*logger_test\.go:\d+: PANIC xyzzy`)
+}
+
+func (s *LogSuite) TestSyslogFails(c *C) {
+ var logbuf bytes.Buffer
+
+ // pretend syslog is not available (e.g. because of no /dev/log in
+ // a chroot or something)
+ newSyslog = func() (*log.Logger, error) {
+ return nil, fmt.Errorf("nih nih")
+ }
+
+ // ensure a warning is displayed
+ l, err := NewConsoleLog(&logbuf, DefaultFlags)
+ c.Assert(err, IsNil)
+ c.Check(logbuf.String(), Matches, `(?m).*:\d+: WARNING: cannot create syslog logger`)
+
+ // ensure that even without a syslog the console log works and we
+ // do not crash
+ logbuf.Reset()
+ SetLogger(l)
+ Noticef("I do not want to crash")
+ c.Check(logbuf.String(), Matches, `(?m).*logger_test\.go:\d+: I do not want to crash`)
+
+}
--- /dev/null
+#!/usr/bin/python
+#
+# see http://daringfireball.net/projects/markdown/syntax
+# for the "canonical" reference
+#
+# We support django-markdown which uses python-markdown, see:
+# http://pythonhosted.org/Markdown/
+
+import sys
+import codecs
+
+def lint_li(fname, text):
+ """Ensure that the list-items are multiplies of 4"""
+ is_clean = True
+ for i, line in enumerate(text.splitlines()):
+ if line.lstrip().startswith("*") and line.index("*") % 4 != 0:
+ print("%s: line %i list has non-4 spaces indent" % (fname, i))
+ is_clean = False
+ return is_clean
+
+
+def lint(md_files):
+ """lint all md files"""
+ all_clean = True
+ for md in md_files:
+ with codecs.open(md, "r", "utf-8") as f:
+ buf = f.read()
+ for fname, func in globals().items():
+ if fname.startswith("lint_"):
+ all_clean &= func(md, buf)
+ return all_clean
+
+
+if __name__ == "__main__":
+ if not lint(sys.argv):
+ sys.exit(1)
--- /dev/null
+#!/bin/sh
+set -e
+
+# debugging if anything fails is tricky as dh-golang eats up all output
+# uncomment the lines below to get a useful trace if you have to touch
+# this again (my advice is: DON'T)
+#set -x
+#logfile=/tmp/mkversions.log
+#exec >> $logfile 2>&1
+#echo "env: $(set)"
+#echo "mkversion.sh run from: $0"
+#echo "pwd: $(pwd)"
+
+# we have two directories we need to care about:
+# - our toplevel pkg builddir which is where "mkversion.sh" is located
+# and where "snap-confine" expects its cmd/VERSION file
+# - the GO_GENERATE_BUILDDIR which may be the toplevel pkg dir. but
+# during "dpkg-buildpackage" it will become a different _build/ dir
+# that dh-golang creates and that only contains a subset of the
+# files of the toplevel buildir.
+PKG_BUILDDIR=$(dirname "$0")
+GO_GENERATE_BUILDDIR="$(pwd)"
+
+# run from "go generate" adjust path
+if [ "$GOPACKAGE" = "cmd" ]; then
+ GO_GENERATE_BUILDDIR="$(pwd)/.."
+fi
+
+if which git >/dev/null; then
+ v="$(git describe --dirty --always | sed -e 's/-/+git/;y/-/./' )"
+ o=git
+fi
+
+if [ -z "$v" ]; then
+ # at this point we maybe in _build/src/github etc where we have no
+ # debian/changelog (dh-golang only exports the sources here)
+ # switch to the real source dir for the changelog parsing
+ v="$(cd ${PKG_BUILDDIR}; dpkg-parsechangelog --show-field Version)";
+ o=debian/changelog
+fi
+
+if [ -z "$v" ]; then
+ exit 1
+fi
+
+echo "*** Setting version to '$v' from $o." >&2
+
+cat <<EOF > ${GO_GENERATE_BUILDDIR}/cmd/version_generated.go
+package cmd
+
+// generated by mkversion.sh; do not edit
+
+func init() {
+ Version = "$v"
+}
+EOF
+
+cat <<EOF > ${PKG_BUILDDIR}/cmd/VERSION
+$v
+EOF
+
+cat <<EOF >${PKG_BUILDDIR}/data/info
+VERSION=$v
+EOF
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ // from /usr/include/linux/fs.h
+ FS_SECRM_FL = 0x00000001 /* Secure deletion */
+ FS_UNRM_FL = 0x00000002 /* Undelete */
+ FS_COMPR_FL = 0x00000004 /* Compress file */
+ FS_SYNC_FL = 0x00000008 /* Synchronous updates */
+ FS_IMMUTABLE_FL = 0x00000010 /* Immutable file */
+ FS_APPEND_FL = 0x00000020 /* writes to file may only append */
+ FS_NODUMP_FL = 0x00000040 /* do not dump file */
+ FS_NOATIME_FL = 0x00000080 /* do not update atime */
+ FS_DIRTY_FL = 0x00000100
+ FS_COMPRBLK_FL = 0x00000200 /* One or more compressed clusters */
+ FS_NOCOMP_FL = 0x00000400 /* Don't compress */
+ FS_ECOMPR_FL = 0x00000800 /* Compression error */
+ FS_BTREE_FL = 0x00001000 /* btree format dir */
+ FS_INDEX_FL = 0x00001000 /* hash-indexed directory */
+ FS_IMAGIC_FL = 0x00002000 /* AFS directory */
+ FS_JOURNAL_DATA_FL = 0x00004000 /* Reserved for ext3 */
+ FS_NOTAIL_FL = 0x00008000 /* file tail should not be merged */
+ FS_DIRSYNC_FL = 0x00010000 /* dirsync behaviour (directories only) */
+ FS_TOPDIR_FL = 0x00020000 /* Top of directory hierarchies*/
+ FS_EXTENT_FL = 0x00080000 /* Extents */
+ FS_DIRECTIO_FL = 0x00100000 /* Use direct i/o */
+ FS_NOCOW_FL = 0x00800000 /* Do not cow file */
+ FS_PROJINHERIT_FL = 0x20000000 /* Create with parents projid */
+ FS_RESERVED_FL = 0x80000000 /* reserved for ext2 lib */
+)
+
+func ioctl(f *os.File, request uintptr, attrp *int32) error {
+ argp := uintptr(unsafe.Pointer(attrp))
+ _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, argp)
+ if errno != 0 {
+ return os.NewSyscallError("ioctl", errno)
+ }
+
+ return nil
+}
+
+// GetAttr retrieves the attributes of a file on a linux filesystem
+func GetAttr(f *os.File) (int32, error) {
+ attr := int32(-1)
+ err := ioctl(f, _FS_IOC_GETFLAGS, &attr)
+ return attr, err
+}
+
+// SetAttr sets the attributes of a file on a linux filesystem to the given value
+func SetAttr(f *os.File, attr int32) error {
+ return ioctl(f, _FS_IOC_SETFLAGS, &attr)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build arm 386 ppc
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+const (
+ // these are actually _FS_IOC32 (i'm cheating)
+ _FS_IOC_GETFLAGS = uintptr(0x80046601)
+ _FS_IOC_SETFLAGS = uintptr(0x40046602)
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build arm64 amd64 ppc64le s390x
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+const (
+ // There is a logic to these but I don't care to implement it all.
+ // If you do, chase them from linux/fs.h
+ _FS_IOC_GETFLAGS = uintptr(0x80086601)
+ _FS_IOC_SETFLAGS = uintptr(0x40086602)
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+)
+
+// ChDir runs runs "f" inside the given directory
+// Note that this will only work reliable in a single-threaded context.
+func ChDir(newDir string, f func() error) (err error) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ if err := os.Chdir(newDir); err != nil {
+ return err
+ }
+ defer os.Chdir(cwd)
+ return f()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "fmt"
+ "os"
+
+ . "gopkg.in/check.v1"
+)
+
+type ChdirTestSuite struct{}
+
+var _ = Suite(&ChdirTestSuite{})
+
+func (ts *ChdirTestSuite) TestChdir(c *C) {
+ tmpdir := c.MkDir()
+
+ cwd, err := os.Getwd()
+ c.Assert(err, IsNil)
+ c.Assert(cwd, Not(Equals), tmpdir)
+ ChDir(tmpdir, func() error {
+ cwd, err := os.Getwd()
+ c.Assert(err, IsNil)
+ c.Assert(cwd, Equals, tmpdir)
+ return err
+ })
+}
+
+func (ts *ChdirTestSuite) TestChdirErrorNoDir(c *C) {
+ err := ChDir("random-dir-that-does-not-exist", func() error {
+ return nil
+ })
+ c.Assert(err, ErrorMatches, "chdir .*: no such file or directory")
+}
+
+func (ts *ChdirTestSuite) TestChdirErrorFromFunc(c *C) {
+ err := ChDir("/", func() error {
+ return fmt.Errorf("meep")
+ })
+ c.Assert(err, ErrorMatches, "meep")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "bytes"
+ "io"
+ "os"
+)
+
+const defaultBufsz = 16 * 1024
+
+var bufsz = defaultBufsz
+
+// FilesAreEqual compares the two files' contents and returns whether
+// they are the same.
+func FilesAreEqual(a, b string) bool {
+ fa, err := os.Open(a)
+ if err != nil {
+ return false
+ }
+ defer fa.Close()
+
+ fb, err := os.Open(b)
+ if err != nil {
+ return false
+ }
+ defer fb.Close()
+
+ fia, err := fa.Stat()
+ if err != nil {
+ return false
+ }
+
+ fib, err := fb.Stat()
+ if err != nil {
+ return false
+ }
+
+ if fia.Size() != fib.Size() {
+ return false
+ }
+
+ return streamsEqual(fa, fb)
+}
+
+func streamsEqual(fa, fb io.Reader) bool {
+ bufa := make([]byte, bufsz)
+ bufb := make([]byte, bufsz)
+ for {
+ ra, erra := io.ReadAtLeast(fa, bufa, bufsz)
+ rb, errb := io.ReadAtLeast(fb, bufb, bufsz)
+ if erra == io.EOF && errb == io.EOF {
+ return true
+ }
+ if erra != nil || errb != nil {
+ // if both files finished in the middle of a
+ // ReadAtLeast, (returning io.ErrUnexpectedEOF), then we
+ // still need to check what was read to know whether
+ // they're equal. Otherwise, we know they're not equal
+ // (because we count any read error as a being non-equal
+ // also).
+ tailMightBeEqual := erra == io.ErrUnexpectedEOF && errb == io.ErrUnexpectedEOF
+ if !tailMightBeEqual {
+ return false
+ }
+ }
+ if !bytes.Equal(bufa[:ra], bufb[:rb]) {
+ return false
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ . "gopkg.in/check.v1"
+)
+
+type CmpTestSuite struct{}
+
+var _ = Suite(&CmpTestSuite{})
+
+func (ts *CmpTestSuite) TestCmp(c *C) {
+ tmpdir := c.MkDir()
+
+ foo := filepath.Join(tmpdir, "foo")
+ f, err := os.Create(foo)
+ c.Assert(err, IsNil)
+ defer f.Close()
+
+ // pick a smaller bufsize so that the test can complete quicker
+ defer func() {
+ bufsz = defaultBufsz
+ }()
+ bufsz = 128
+
+ // test FilesAreEqual for various sizes:
+ // - bufsz not exceeded
+ // - bufsz matches file size
+ // - bufsz exceeds file size
+ canary := "1234567890123456"
+ for _, n := range []int{1, bufsz / len(canary), (bufsz / len(canary)) + 1} {
+ for i := 0; i < n; i++ {
+ c.Assert(FilesAreEqual(foo, foo), Equals, true)
+ _, err := f.WriteString(canary)
+ c.Assert(err, IsNil)
+ f.Sync()
+ }
+ }
+}
+
+func (ts *CmpTestSuite) TestCmpEmptyNeqMissing(c *C) {
+ tmpdir := c.MkDir()
+
+ foo := filepath.Join(tmpdir, "foo")
+ bar := filepath.Join(tmpdir, "bar")
+ f, err := os.Create(foo)
+ c.Assert(err, IsNil)
+ defer f.Close()
+ c.Assert(FilesAreEqual(foo, bar), Equals, false)
+ c.Assert(FilesAreEqual(bar, foo), Equals, false)
+}
+
+func (ts *CmpTestSuite) TestCmpEmptyNeqNonEmpty(c *C) {
+ tmpdir := c.MkDir()
+
+ foo := filepath.Join(tmpdir, "foo")
+ bar := filepath.Join(tmpdir, "bar")
+ f, err := os.Create(foo)
+ c.Assert(err, IsNil)
+ defer f.Close()
+ c.Assert(ioutil.WriteFile(bar, []byte("x"), 0644), IsNil)
+ c.Assert(FilesAreEqual(foo, bar), Equals, false)
+ c.Assert(FilesAreEqual(bar, foo), Equals, false)
+}
+
+func (ts *CmpTestSuite) TestCmpStreams(c *C) {
+ for _, x := range []struct {
+ a string
+ b string
+ r bool
+ }{
+ {"hello", "hello", true},
+ {"hello", "world", false},
+ {"hello", "hell", false},
+ } {
+ c.Assert(streamsEqual(strings.NewReader(x.a), strings.NewReader(x.b)), Equals, x.r)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+// CopyFlag is used to tweak the behaviour of CopyFile
+type CopyFlag uint8
+
+const (
+ // CopyFlagDefault is the default behaviour
+ CopyFlagDefault CopyFlag = 0
+ // CopyFlagSync does a sync after copying the files
+ CopyFlagSync CopyFlag = 1 << iota
+ // CopyFlagOverwrite overwrites the target if it exists
+ CopyFlagOverwrite
+ // CopyFlagPreserveAll preserves mode,owner,time attributes
+ CopyFlagPreserveAll
+)
+
+var (
+ openfile = doOpenFile
+ copyfile = doCopyFile
+)
+
+type fileish interface {
+ Close() error
+ Sync() error
+ Fd() uintptr
+ Stat() (os.FileInfo, error)
+ Read([]byte) (int, error)
+ Write([]byte) (int, error)
+}
+
+func doOpenFile(name string, flag int, perm os.FileMode) (fileish, error) {
+ return os.OpenFile(name, flag, perm)
+}
+
+// CopyFile copies src to dst
+func CopyFile(src, dst string, flags CopyFlag) (err error) {
+ if flags&CopyFlagPreserveAll != 0 {
+ // Our native copy code does not preserve all attributes
+ // (yet). If the user needs this functionatlity we just
+ // fallback to use the system's "cp" binary to do the copy.
+ if err := runCpPreserveAll(src, dst, "copy all"); err != nil {
+ return err
+ }
+ if flags&CopyFlagSync != 0 {
+ return runSync()
+ }
+ return nil
+ }
+
+ fin, err := openfile(src, os.O_RDONLY, 0)
+ if err != nil {
+ return fmt.Errorf("unable to open %s: %v", src, err)
+ }
+ defer func() {
+ if cerr := fin.Close(); cerr != nil && err == nil {
+ err = fmt.Errorf("when closing %s: %v", src, cerr)
+ }
+ }()
+
+ fi, err := fin.Stat()
+ if err != nil {
+ return fmt.Errorf("unable to stat %s: %v", src, err)
+ }
+
+ outflags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
+ if flags&CopyFlagOverwrite == 0 {
+ outflags |= os.O_EXCL
+ }
+
+ fout, err := openfile(dst, outflags, fi.Mode())
+ if err != nil {
+ return fmt.Errorf("unable to create %s: %v", dst, err)
+ }
+ defer func() {
+ if cerr := fout.Close(); cerr != nil && err == nil {
+ err = fmt.Errorf("when closing %s: %v", dst, cerr)
+ }
+ }()
+
+ if err := copyfile(fin, fout, fi); err != nil {
+ return fmt.Errorf("unable to copy %s to %s: %v", src, dst, err)
+ }
+
+ if flags&CopyFlagSync != 0 {
+ if err = fout.Sync(); err != nil {
+ return fmt.Errorf("unable to sync %s: %v", dst, err)
+ }
+ }
+
+ return nil
+}
+
+func runCmd(cmd *exec.Cmd, errdesc string) error {
+ if output, err := cmd.CombinedOutput(); err != nil {
+ output = bytes.TrimSpace(output)
+ if exitCode, err := ExitCode(err); err == nil {
+ return &ErrCopySpecialFile{
+ desc: errdesc,
+ exitCode: exitCode,
+ output: output,
+ }
+ }
+ return &ErrCopySpecialFile{
+ desc: errdesc,
+ err: err,
+ output: output,
+ }
+ }
+
+ return nil
+}
+
+func runSync(args ...string) error {
+ return runCmd(exec.Command("sync", args...), "sync")
+}
+
+func runCpPreserveAll(path, dest, errdesc string) error {
+ return runCmd(exec.Command("cp", "-av", path, dest), errdesc)
+}
+
+// CopySpecialFile is used to copy all the things that are not files
+// (like device nodes, named pipes etc)
+func CopySpecialFile(path, dest string) error {
+ if err := runCpPreserveAll(path, dest, "copy device node"); err != nil {
+ return err
+ }
+ return runSync(filepath.Dir(dest))
+}
+
+// ErrCopySpecialFile is returned if a special file copy fails
+type ErrCopySpecialFile struct {
+ desc string
+ exitCode int
+ output []byte
+ err error
+}
+
+func (e ErrCopySpecialFile) Error() string {
+ if e.err == nil {
+ return fmt.Sprintf("failed to %s: %q (%v)", e.desc, e.output, e.exitCode)
+ }
+
+ return fmt.Sprintf("failed to %s: %q (%v)", e.desc, e.output, e.err)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+ "syscall"
+)
+
+const maxint = int64(^uint(0) >> 1)
+
+var maxcp = maxint // overridden in testing
+
+func doCopyFile(fin, fout fileish, fi os.FileInfo) error {
+ size := fi.Size()
+ var offset int64
+ for offset < size {
+ // sendfile is funny; it only copies up to maxint
+ // bytes at a time, but takes an int64 offset.
+ count := size - offset
+ if count > maxcp {
+ count = maxcp
+ }
+
+ if _, err := syscall.Sendfile(int(fout.Fd()), int(fin.Fd()), &offset, int(count)); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "io/ioutil"
+ "os"
+
+ . "gopkg.in/check.v1"
+)
+
+func (s *cpSuite) TestCpMulti(c *C) {
+ maxcp = 2
+ defer func() { maxcp = maxint }()
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagDefault), IsNil)
+ bs, err := ioutil.ReadFile(s.f2)
+ c.Check(err, IsNil)
+ c.Check(bs, DeepEquals, s.data)
+}
+
+func (s *cpSuite) TestDoCpErr(c *C) {
+ f1, err := os.Open(s.f1)
+ c.Assert(err, IsNil)
+ st, err := f1.Stat()
+ c.Assert(err, IsNil)
+ // force an error by asking it to write to a readonly stream
+ c.Check(doCopyFile(f1, os.Stdin, st), NotNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build !linux
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "io"
+ "os"
+)
+
+func doCopyFile(fin, fout fileish, fi os.FileInfo) error {
+ _, err := io.Copy(fout, fin)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/testutil"
+)
+
+type cpSuite struct {
+ dir string
+ f1 string
+ f2 string
+ data []byte
+ log []string
+ errs []error
+ idx int
+}
+
+var _ = Suite(&cpSuite{})
+
+func (s *cpSuite) mockCopyFile(fin, fout fileish, fi os.FileInfo) error {
+ return s.µ("copyfile")
+}
+
+func (s *cpSuite) mockOpenFile(name string, flag int, perm os.FileMode) (fileish, error) {
+ return &mockfile{s}, s.µ("open")
+}
+
+func (s *cpSuite) µ(msg string) (err error) {
+ s.log = append(s.log, msg)
+ if len(s.errs) > 0 {
+ err = s.errs[0]
+ if len(s.errs) > 1 {
+ s.errs = s.errs[1:]
+ }
+ }
+
+ return
+}
+
+func (s *cpSuite) SetUpTest(c *C) {
+ s.errs = nil
+ s.log = nil
+ s.dir = c.MkDir()
+ s.f1 = filepath.Join(s.dir, "f1")
+ s.f2 = filepath.Join(s.dir, "f2")
+ s.data = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
+ c.Assert(ioutil.WriteFile(s.f1, s.data, 0644), IsNil)
+}
+
+func (s *cpSuite) mock() {
+ copyfile = s.mockCopyFile
+ openfile = s.mockOpenFile
+}
+
+func (s *cpSuite) TearDownTest(c *C) {
+ copyfile = doCopyFile
+ openfile = doOpenFile
+}
+
+func (s *cpSuite) TestCp(c *C) {
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagDefault), IsNil)
+ bs, err := ioutil.ReadFile(s.f2)
+ c.Check(err, IsNil)
+ c.Check(bs, DeepEquals, s.data)
+}
+
+func (s *cpSuite) TestCpNoOverwrite(c *C) {
+ _, err := os.Create(s.f2)
+ c.Assert(err, IsNil)
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagDefault), NotNil)
+}
+
+func (s *cpSuite) TestCpOverwrite(c *C) {
+ _, err := os.Create(s.f2)
+ c.Assert(err, IsNil)
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagOverwrite), IsNil)
+ bs, err := ioutil.ReadFile(s.f2)
+ c.Check(err, IsNil)
+ c.Check(bs, DeepEquals, s.data)
+}
+
+func (s *cpSuite) TestCpOverwriteTruncates(c *C) {
+ c.Assert(ioutil.WriteFile(s.f2, []byte("xxxxxxxxxxxxxxxx"), 0644), IsNil)
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagOverwrite), IsNil)
+ bs, err := ioutil.ReadFile(s.f2)
+ c.Check(err, IsNil)
+ c.Check(bs, DeepEquals, s.data)
+}
+
+func (s *cpSuite) TestCpSync(c *C) {
+ s.mock()
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagDefault), IsNil)
+ c.Check(strings.Join(s.log, ":"), Not(Matches), `.*:sync(:.*)?`)
+
+ s.log = nil
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), IsNil)
+ c.Check(strings.Join(s.log, ":"), Matches, `(.*:)?sync(:.*)?`)
+}
+
+func (s *cpSuite) TestCpCantOpen(c *C) {
+ s.mock()
+ s.errs = []error{errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `unable to open \S+/f1: xyzzy`)
+}
+
+func (s *cpSuite) TestCpCantStat(c *C) {
+ s.mock()
+ s.errs = []error{nil, errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `unable to stat \S+/f1: xyzzy`)
+}
+
+func (s *cpSuite) TestCpCantCreate(c *C) {
+ s.mock()
+ s.errs = []error{nil, nil, errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `unable to create \S+/f2: xyzzy`)
+}
+
+func (s *cpSuite) TestCpCantCopy(c *C) {
+ s.mock()
+ s.errs = []error{nil, nil, nil, errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `unable to copy \S+/f1 to \S+/f2: xyzzy`)
+}
+
+func (s *cpSuite) TestCpCantSync(c *C) {
+ s.mock()
+ s.errs = []error{nil, nil, nil, nil, errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `unable to sync \S+/f2: xyzzy`)
+}
+
+func (s *cpSuite) TestCpCantStop2(c *C) {
+ s.mock()
+ s.errs = []error{nil, nil, nil, nil, nil, errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `when closing \S+/f2: xyzzy`)
+}
+
+func (s *cpSuite) TestCpCantStop1(c *C) {
+ s.mock()
+ s.errs = []error{nil, nil, nil, nil, nil, nil, errors.New("xyzzy"), nil}
+
+ c.Check(CopyFile(s.f1, s.f2, CopyFlagSync), ErrorMatches, `when closing \S+/f1: xyzzy`)
+}
+
+type mockfile struct {
+ s *cpSuite
+}
+
+var mockst = mockstat{}
+
+func (f *mockfile) Close() error { return f.s.µ("close") }
+func (f *mockfile) Sync() error { return f.s.µ("sync") }
+func (f *mockfile) Fd() uintptr { f.s.µ("fd"); return 42 }
+func (f *mockfile) Read([]byte) (int, error) { return 0, f.s.µ("read") }
+func (f *mockfile) Write([]byte) (int, error) { return 0, f.s.µ("write") }
+func (f *mockfile) Stat() (os.FileInfo, error) { return mockst, f.s.µ("stat") }
+
+type mockstat struct{}
+
+func (mockstat) Name() string { return "mockstat" }
+func (mockstat) Size() int64 { return 42 }
+func (mockstat) Mode() os.FileMode { return 0644 }
+func (mockstat) ModTime() time.Time { return time.Now() }
+func (mockstat) IsDir() bool { return false }
+func (mockstat) Sys() interface{} { return nil }
+
+func (s *cpSuite) TestCopySpecialFileSimple(c *C) {
+ sync := testutil.MockCommand(c, "sync", "")
+ defer sync.Restore()
+
+ src := filepath.Join(c.MkDir(), "fifo")
+ err := syscall.Mkfifo(src, 0644)
+ c.Assert(err, IsNil)
+ dir := c.MkDir()
+ dst := filepath.Join(dir, "copied-fifo")
+
+ err = CopySpecialFile(src, dst)
+ c.Assert(err, IsNil)
+
+ st, err := os.Stat(dst)
+ c.Assert(err, IsNil)
+ c.Check((st.Mode() & os.ModeNamedPipe), Equals, os.ModeNamedPipe)
+ c.Check(sync.Calls(), DeepEquals, [][]string{{"sync", dir}})
+}
+
+func (s *cpSuite) TestCopySpecialFileErrors(c *C) {
+ err := CopySpecialFile("no-such-file", "no-such-target")
+ c.Assert(err, ErrorMatches, "failed to copy device node:.*cp:.*stat.*no-such-file.*")
+}
+
+func (s *cpSuite) TestCopyPreserveAll(c *C) {
+ src := filepath.Join(c.MkDir(), "meep")
+ dst := filepath.Join(c.MkDir(), "copied-meep")
+
+ err := ioutil.WriteFile(src, []byte(nil), 0644)
+ c.Assert(err, IsNil)
+
+ // Give the file a different mtime to ensure CopyFlagPreserveAll
+ // really works.
+ //
+ // You wonder why "touch" is used? And want to me about
+ // syscall.Utime()? Well, syscall not implemented on armhf
+ // Aha, syscall.Utimes() then? No, not implemented on arm64
+ // Really, this is a just a test, touch is good enough!
+ err = exec.Command("touch", src, "-d", "2007-08-23 08:21:42").Run()
+ c.Assert(err, IsNil)
+
+ err = CopyFile(src, dst, CopyFlagPreserveAll)
+ c.Assert(err, IsNil)
+
+ // ensure that the mtime got preserved
+ st1, err := os.Stat(src)
+ c.Assert(err, IsNil)
+ st2, err := os.Stat(dst)
+ c.Assert(err, IsNil)
+ c.Assert(st1.ModTime(), Equals, st2.ModTime())
+}
+
+func (s *cpSuite) TestCopyPreserveAllSync(c *C) {
+ dir := c.MkDir()
+ mocked := testutil.MockCommand(c, "cp", "").Also("sync", "")
+ defer mocked.Restore()
+
+ src := filepath.Join(dir, "meep")
+ dst := filepath.Join(dir, "copied-meep")
+
+ err := ioutil.WriteFile(src, []byte(nil), 0644)
+ c.Assert(err, IsNil)
+
+ err = CopyFile(src, dst, CopyFlagPreserveAll|CopyFlagSync)
+ c.Assert(err, IsNil)
+
+ c.Check(mocked.Calls(), DeepEquals, [][]string{
+ {"cp", "-av", src, dst},
+ {"sync"},
+ })
+}
+
+func (s *cpSuite) TestCopyPreserveAllSyncCpFailure(c *C) {
+ dir := c.MkDir()
+ mocked := testutil.MockCommand(c, "cp", "echo OUCH: cp failed.;exit 42").Also("sync", "")
+ defer mocked.Restore()
+
+ src := filepath.Join(dir, "meep")
+ dst := filepath.Join(dir, "copied-meep")
+
+ err := ioutil.WriteFile(src, []byte(nil), 0644)
+ c.Assert(err, IsNil)
+
+ err = CopyFile(src, dst, CopyFlagPreserveAll|CopyFlagSync)
+ c.Assert(err, ErrorMatches, `failed to copy all: "OUCH: cp failed." \(42\)`)
+ c.Check(mocked.Calls(), DeepEquals, [][]string{
+ {"cp", "-av", src, dst},
+ })
+}
+
+func (s *cpSuite) TestCopyPreserveAllSyncSyncFailure(c *C) {
+ dir := c.MkDir()
+ mocked := testutil.MockCommand(c, "cp", "").Also("sync", "echo OUCH: sync failed.;exit 42")
+ defer mocked.Restore()
+
+ src := filepath.Join(dir, "meep")
+ dst := filepath.Join(dir, "copied-meep")
+
+ err := ioutil.WriteFile(src, []byte(nil), 0644)
+ c.Assert(err, IsNil)
+
+ err = CopyFile(src, dst, CopyFlagPreserveAll|CopyFlagSync)
+ c.Assert(err, ErrorMatches, `failed to sync: "OUCH: sync failed." \(42\)`)
+
+ c.Check(mocked.Calls(), DeepEquals, [][]string{
+ {"cp", "-av", src, dst},
+ {"sync"},
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "crypto"
+ "io"
+ "os"
+)
+
+const (
+ hashDigestBufSize = 2 * 1024 * 1024
+)
+
+// FileDigest computes a hash digest of the file using the given hash.
+// It also returns the file size.
+func FileDigest(filename string, hash crypto.Hash) ([]byte, uint64, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer f.Close()
+ h := hash.New()
+ size, err := io.CopyBuffer(h, f, make([]byte, hashDigestBufSize))
+ if err != nil {
+ return nil, 0, err
+ }
+ return h.Sum(nil), uint64(size), nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil_test
+
+import (
+ "crypto"
+ "crypto/sha512"
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+type FileDigestSuite struct{}
+
+var _ = Suite(&FileDigestSuite{})
+
+func (ts *FileDigestSuite) TestFileDigest(c *C) {
+ exData := []byte("hashmeplease")
+
+ tempdir := c.MkDir()
+ fn := filepath.Join(tempdir, "ex.file")
+ err := ioutil.WriteFile(fn, exData, 0644)
+ c.Assert(err, IsNil)
+
+ digest, size, err := osutil.FileDigest(fn, crypto.SHA512)
+ c.Assert(err, IsNil)
+ c.Check(size, Equals, uint64(len(exData)))
+ h512 := sha512.Sum512(exData)
+ c.Check(digest, DeepEquals, h512[:])
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+ "strconv"
+)
+
+// GetenvBool returns whether the given key may be considered "set" in the
+// environment (i.e. it is set to one of "1", "true", etc).
+//
+// An optional second argument can be provided, which determines how to
+// treat missing or unparsable values; default is to treat them as false.
+func GetenvBool(key string, dflt ...bool) bool {
+ val := os.Getenv(key)
+ if val == "" {
+ if len(dflt) > 0 {
+ return dflt[0]
+ }
+
+ return false
+ }
+
+ b, err := strconv.ParseBool(val)
+ if err != nil {
+ if len(dflt) > 0 {
+ return dflt[0]
+ }
+
+ return false
+ }
+
+ return b
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil_test
+
+import (
+ "os"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+type envSuite struct{}
+
+var _ = check.Suite(&envSuite{})
+
+func (s *envSuite) TestGetenvBoolTrue(c *check.C) {
+ key := "__XYZZY__"
+ os.Unsetenv(key)
+
+ for _, s := range []string{
+ "1", "t", "TRUE",
+ } {
+ os.Setenv(key, s)
+ c.Assert(os.Getenv(key), check.Equals, s)
+ c.Check(osutil.GetenvBool(key), check.Equals, true, check.Commentf(s))
+ c.Check(osutil.GetenvBool(key, false), check.Equals, true, check.Commentf(s))
+ c.Check(osutil.GetenvBool(key, true), check.Equals, true, check.Commentf(s))
+ }
+}
+
+func (s *envSuite) TestGetenvBoolFalse(c *check.C) {
+ key := "__XYZZY__"
+ os.Unsetenv(key)
+ c.Assert(osutil.GetenvBool(key), check.Equals, false)
+
+ for _, s := range []string{
+ "", "0", "f", "FALSE", "potato",
+ } {
+ os.Setenv(key, s)
+ c.Assert(os.Getenv(key), check.Equals, s)
+ c.Check(osutil.GetenvBool(key), check.Equals, false, check.Commentf(s))
+ c.Check(osutil.GetenvBool(key, false), check.Equals, false, check.Commentf(s))
+ }
+}
+
+func (s *envSuite) TestGetenvBoolFalseDefaultTrue(c *check.C) {
+ key := "__XYZZY__"
+ os.Unsetenv(key)
+ c.Assert(osutil.GetenvBool(key), check.Equals, false)
+
+ for _, s := range []string{
+ "0", "f", "FALSE",
+ } {
+ os.Setenv(key, s)
+ c.Assert(os.Getenv(key), check.Equals, s)
+ c.Check(osutil.GetenvBool(key, true), check.Equals, false, check.Commentf(s))
+ }
+
+ for _, s := range []string{
+ "", "potato", // etc
+ } {
+ os.Setenv(key, s)
+ c.Assert(os.Getenv(key), check.Equals, s)
+ c.Check(osutil.GetenvBool(key, true), check.Equals, true, check.Commentf(s))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os/exec"
+ "syscall"
+)
+
+// ExitCode extract the exit code from the error of a failed cmd.Run() or the
+// original error if its not a exec.ExitError
+func ExitCode(runErr error) (e int, err error) {
+ // golang, you are kidding me, right?
+ if exitErr, ok := runErr.(*exec.ExitError); ok {
+ waitStatus := exitErr.Sys().(syscall.WaitStatus)
+ e = waitStatus.ExitStatus()
+ return e, nil
+ }
+ return e, runErr
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+ "os/exec"
+
+ . "gopkg.in/check.v1"
+)
+
+type ExitCodeTestSuite struct{}
+
+var _ = Suite(&ExitCodeTestSuite{})
+
+func (ts *ExitCodeTestSuite) TestExitCode(c *C) {
+ cmd := exec.Command("true")
+ err := cmd.Run()
+ c.Assert(err, IsNil)
+
+ cmd = exec.Command("false")
+ err = cmd.Run()
+ c.Assert(err, NotNil)
+ e, err := ExitCode(err)
+ c.Assert(err, IsNil)
+ c.Assert(e, Equals, 1)
+
+ cmd = exec.Command("sh", "-c", "exit 7")
+ err = cmd.Run()
+ e, err = ExitCode(err)
+ c.Assert(err, IsNil)
+ c.Assert(e, Equals, 7)
+
+ // ensure that non exec.ExitError values give a error
+ _, err = os.Stat("/random/file/that/is/not/there")
+ c.Assert(err, NotNil)
+ _, err = ExitCode(err)
+ c.Assert(err, NotNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os/user"
+)
+
+func MockUserLookup(mock func(name string) (*user.User, error)) func() {
+ realUserLookup := userLookup
+ userLookup = mock
+
+ return func() { userLookup = realUserLookup }
+}
+
+func MockUserCurrent(mock func() (*user.User, error)) func() {
+ realUserCurrent := userCurrent
+ userCurrent = mock
+
+ return func() { userCurrent = realUserCurrent }
+}
+
+func MockSudoersDotD(mockDir string) func() {
+ realSudoersD := sudoersDotD
+ sudoersDotD = mockDir
+
+ return func() { sudoersDotD = realSudoersD }
+}
+
+func MockMountInfoPath(mockMountInfoPath string) func() {
+ realMountInfoPath := mountInfoPath
+ mountInfoPath = mockMountInfoPath
+
+ return func() { mountInfoPath = realMountInfoPath }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/strutil"
+)
+
+// AtomicWriteFlags are a bitfield of flags for AtomicWriteFile
+type AtomicWriteFlags uint
+
+const (
+ // AtomicWriteFollow makes AtomicWriteFile follow symlinks
+ AtomicWriteFollow AtomicWriteFlags = 1 << iota
+)
+
+// AtomicWriteFile updates the filename atomically and works otherwise
+// like io/ioutil.WriteFile()
+//
+// Note that it won't follow symlinks and will replace existing symlinks
+// with the real file
+func AtomicWriteFile(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags) (err error) {
+ return AtomicWriteFileChown(filename, data, perm, flags, -1, -1)
+}
+
+func AtomicWriteFileChown(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags, uid, gid int) (err error) {
+ if flags&AtomicWriteFollow != 0 {
+ if fn, err := os.Readlink(filename); err == nil || (fn != "" && os.IsNotExist(err)) {
+ if filepath.IsAbs(fn) {
+ filename = fn
+ } else {
+ filename = filepath.Join(filepath.Dir(filename), fn)
+ }
+ }
+ }
+ tmp := filename + "." + strutil.MakeRandomString(12)
+
+ // XXX: if go switches to use aio_fsync, we need to open the dir for writing
+ dir, err := os.Open(filepath.Dir(filename))
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+
+ fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, perm)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ e := fd.Close()
+ if err == nil {
+ err = e
+ }
+ if err != nil {
+ os.Remove(tmp)
+ }
+ }()
+
+ // according to the docs, Write returns a non-nil error when n !=
+ // len(b), so don't worry about short writes.
+ if _, err := fd.Write(data); err != nil {
+ return err
+ }
+
+ if uid > -1 && gid > -1 {
+ if err := fd.Chown(uid, gid); err != nil {
+ return err
+ }
+ } else if uid > -1 || gid > -1 {
+ return errors.New("internal error: AtomicWriteFileChown needs none or both of uid and gid set")
+ }
+
+ if err := fd.Sync(); err != nil {
+ return err
+ }
+
+ if err := os.Rename(tmp, filename); err != nil {
+ return err
+ }
+
+ return dir.Sync()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "io/ioutil"
+ "math/rand"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/strutil"
+
+ . "gopkg.in/check.v1"
+)
+
+type AtomicWriteTestSuite struct{}
+
+var _ = Suite(&AtomicWriteTestSuite{})
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFile(c *C) {
+ tmpdir := c.MkDir()
+
+ p := filepath.Join(tmpdir, "foo")
+ err := AtomicWriteFile(p, []byte("canary"), 0644, 0)
+ c.Assert(err, IsNil)
+
+ content, err := ioutil.ReadFile(p)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "canary")
+
+ // no files left behind!
+ d, err := ioutil.ReadDir(tmpdir)
+ c.Assert(err, IsNil)
+ c.Assert(len(d), Equals, 1)
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFilePermissions(c *C) {
+ tmpdir := c.MkDir()
+
+ p := filepath.Join(tmpdir, "foo")
+ err := AtomicWriteFile(p, []byte(""), 0600, 0)
+ c.Assert(err, IsNil)
+
+ st, err := os.Stat(p)
+ c.Assert(err, IsNil)
+ c.Assert(st.Mode()&os.ModePerm, Equals, os.FileMode(0600))
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileOverwrite(c *C) {
+ tmpdir := c.MkDir()
+ p := filepath.Join(tmpdir, "foo")
+ c.Assert(ioutil.WriteFile(p, []byte("hello"), 0644), IsNil)
+ c.Assert(AtomicWriteFile(p, []byte("hi"), 0600, 0), IsNil)
+
+ content, err := ioutil.ReadFile(p)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "hi")
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileSymlinkNoFollow(c *C) {
+ tmpdir := c.MkDir()
+ rodir := filepath.Join(tmpdir, "ro")
+ p := filepath.Join(rodir, "foo")
+ s := filepath.Join(tmpdir, "foo")
+ c.Assert(os.MkdirAll(rodir, 0755), IsNil)
+ c.Assert(os.Symlink(s, p), IsNil)
+ c.Assert(os.Chmod(rodir, 0500), IsNil)
+ defer os.Chmod(rodir, 0700)
+
+ err := AtomicWriteFile(p, []byte("hi"), 0600, 0)
+ c.Assert(err, NotNil)
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileAbsoluteSymlinks(c *C) {
+ tmpdir := c.MkDir()
+ rodir := filepath.Join(tmpdir, "ro")
+ p := filepath.Join(rodir, "foo")
+ s := filepath.Join(tmpdir, "foo")
+ c.Assert(os.MkdirAll(rodir, 0755), IsNil)
+ c.Assert(os.Symlink(s, p), IsNil)
+ c.Assert(os.Chmod(rodir, 0500), IsNil)
+ defer os.Chmod(rodir, 0700)
+
+ err := AtomicWriteFile(p, []byte("hi"), 0600, AtomicWriteFollow)
+ c.Assert(err, IsNil)
+
+ content, err := ioutil.ReadFile(p)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "hi")
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileOverwriteAbsoluteSymlink(c *C) {
+ tmpdir := c.MkDir()
+ rodir := filepath.Join(tmpdir, "ro")
+ p := filepath.Join(rodir, "foo")
+ s := filepath.Join(tmpdir, "foo")
+ c.Assert(os.MkdirAll(rodir, 0755), IsNil)
+ c.Assert(os.Symlink(s, p), IsNil)
+ c.Assert(os.Chmod(rodir, 0500), IsNil)
+ defer os.Chmod(rodir, 0700)
+
+ c.Assert(ioutil.WriteFile(s, []byte("hello"), 0644), IsNil)
+ c.Assert(AtomicWriteFile(p, []byte("hi"), 0600, AtomicWriteFollow), IsNil)
+
+ content, err := ioutil.ReadFile(p)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "hi")
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileRelativeSymlinks(c *C) {
+ tmpdir := c.MkDir()
+ rodir := filepath.Join(tmpdir, "ro")
+ p := filepath.Join(rodir, "foo")
+ c.Assert(os.MkdirAll(rodir, 0755), IsNil)
+ c.Assert(os.Symlink("../foo", p), IsNil)
+ c.Assert(os.Chmod(rodir, 0500), IsNil)
+ defer os.Chmod(rodir, 0700)
+
+ err := AtomicWriteFile(p, []byte("hi"), 0600, AtomicWriteFollow)
+ c.Assert(err, IsNil)
+
+ content, err := ioutil.ReadFile(p)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "hi")
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileOverwriteRelativeSymlink(c *C) {
+ tmpdir := c.MkDir()
+ rodir := filepath.Join(tmpdir, "ro")
+ p := filepath.Join(rodir, "foo")
+ s := filepath.Join(tmpdir, "foo")
+ c.Assert(os.MkdirAll(rodir, 0755), IsNil)
+ c.Assert(os.Symlink("../foo", p), IsNil)
+ c.Assert(os.Chmod(rodir, 0500), IsNil)
+ defer os.Chmod(rodir, 0700)
+
+ c.Assert(ioutil.WriteFile(s, []byte("hello"), 0644), IsNil)
+ c.Assert(AtomicWriteFile(p, []byte("hi"), 0600, AtomicWriteFollow), IsNil)
+
+ content, err := ioutil.ReadFile(p)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "hi")
+}
+
+func (ts *AtomicWriteTestSuite) TestAtomicWriteFileNoOverwriteTmpExisting(c *C) {
+ tmpdir := c.MkDir()
+ // ensure we always get the same result
+ rand.Seed(1)
+ expectedRandomness := strutil.MakeRandomString(12)
+ // ensure we always get the same result
+ rand.Seed(1)
+
+ p := filepath.Join(tmpdir, "foo")
+ err := ioutil.WriteFile(p+"."+expectedRandomness, []byte(""), 0644)
+ c.Assert(err, IsNil)
+
+ err = AtomicWriteFile(p, []byte(""), 0600, 0)
+ c.Assert(err, ErrorMatches, "open .*: file exists")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+ "path/filepath"
+ "syscall"
+)
+
+// MkdirAllChown is like os.MkdirAll but it calls os.Chown on any
+// directories it creates.
+func MkdirAllChown(path string, perm os.FileMode, uid, gid int) error {
+ if s, err := os.Stat(path); err == nil {
+ if s.IsDir() {
+ return nil
+ }
+
+ // emulate os.MkdirAll
+ return &os.PathError{
+ Op: "mkdir",
+ Path: path,
+ Err: syscall.ENOTDIR,
+ }
+ }
+
+ dir := filepath.Dir(path)
+ if dir != "/" {
+ if err := MkdirAllChown(dir, perm, uid, gid); err != nil {
+ return err
+ }
+ }
+
+ cand := path + ".mkdir-new"
+
+ if err := os.Mkdir(cand, perm); err != nil && !os.IsExist(err) {
+ return err
+ }
+
+ if err := os.Chown(cand, uid, gid); err != nil {
+ return err
+ }
+
+ if err := os.Rename(cand, path); err != nil {
+ return err
+ }
+
+ fd, err := os.Open(dir)
+ if err != nil {
+ return err
+ }
+ defer fd.Close()
+
+ return fd.Sync()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package osutil
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+)
+
+var mountInfoPath = "/proc/self/mountinfo"
+
+func IsMounted(baseDir string) (bool, error) {
+ f, err := os.Open(mountInfoPath)
+ if err != nil {
+ return false, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ l := strings.Fields(scanner.Text())
+ if len(l) == 0 {
+ continue
+ }
+ if len(l) < 7 {
+ return false, fmt.Errorf("unexpected mountinfo line: %q", scanner.Text())
+ }
+ // this parser is simplistic, there are optional fields in
+ // the mountinfo lines, however those are *after* l[4] so
+ // we ignore it for now (because we only care about the
+ // mount point)
+ mountPoint := l[4]
+ if baseDir == mountPoint {
+ return true, nil
+ }
+ }
+
+ return false, scanner.Err()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+type mountSuite struct{}
+
+var _ = Suite(&mountSuite{})
+
+func (s *mountSuite) TestIsMountedHappyish(c *C) {
+ mockMountInfoFn := filepath.Join(c.MkDir(), "mountinfo")
+ restore := osutil.MockMountInfoPath(mockMountInfoFn)
+ defer restore()
+
+ // note the different optinal fields
+ content := []byte(`
+44 24 7:1 / /snap/ubuntu-core/855 rw,relatime shared:27 - squashfs /dev/loop1 ro
+44 24 7:1 / /snap/something/123 rw,relatime - squashfs /dev/loop2 ro
+44 24 7:1 / /snap/random/456 rw,relatime opt:1 shared:27 - squashfs /dev/loop1 ro
+`)
+ err := ioutil.WriteFile(mockMountInfoFn, content, 0644)
+ c.Assert(err, IsNil)
+
+ mounted, err := osutil.IsMounted("/snap/ubuntu-core/855")
+ c.Check(err, IsNil)
+ c.Check(mounted, Equals, true)
+
+ mounted, err = osutil.IsMounted("/snap/something/123")
+ c.Check(err, IsNil)
+ c.Check(mounted, Equals, true)
+
+ mounted, err = osutil.IsMounted("/snap/random/456")
+ c.Check(err, IsNil)
+ c.Check(mounted, Equals, true)
+
+ mounted, err = osutil.IsMounted("/random/made/up/name")
+ c.Check(err, IsNil)
+ c.Check(mounted, Equals, false)
+}
+
+func (s *mountSuite) TestIsMountedNotThereErr(c *C) {
+ restore := osutil.MockMountInfoPath("/no/such/file")
+ defer restore()
+
+ _, err := osutil.IsMounted("/snap/ubuntu-core/855")
+ c.Check(err, ErrorMatches, "open /no/such/file: no such file or directory")
+}
+
+func (s *mountSuite) TestIsMountedIncorrectLines(c *C) {
+ mockMountInfoFn := filepath.Join(c.MkDir(), "mountinfo")
+ restore := osutil.MockMountInfoPath(mockMountInfoFn)
+ defer restore()
+
+ content := []byte(`
+invalid line
+`)
+ err := ioutil.WriteFile(mockMountInfoFn, content, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = osutil.IsMounted("/snap/ubuntu-core/855")
+ c.Check(err, ErrorMatches, `unexpected mountinfo line: "invalid line"`)
+}
--- /dev/null
+package osutil_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "bytes"
+ "fmt"
+)
+
+// OutputErr formats an error based on output if its length is not zero,
+// or returns err otherwise.
+func OutputErr(output []byte, err error) error {
+ output = bytes.TrimSpace(output)
+ if len(output) > 0 {
+ if bytes.Contains(output, []byte{'\n'}) {
+ err = fmt.Errorf("\n-----\n%s\n-----", output)
+ } else {
+ err = fmt.Errorf("%s", output)
+ }
+ }
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+)
+
+type outputErrSuite struct{}
+
+var _ = Suite(&outputErrSuite{})
+
+func (ts *outputErrSuite) TestOutputErrOutputWithoutNewlines(c *C) {
+ output := "test output"
+ err := fmt.Errorf("test error")
+ formattedErr := OutputErr([]byte(output), err)
+ c.Check(formattedErr, ErrorMatches, output)
+}
+
+func (ts *outputErrSuite) TestOutputErrOutputWithNewlines(c *C) {
+ output := "output line1\noutput line2"
+ err := fmt.Errorf("test error")
+ formattedErr := OutputErr([]byte(output), err)
+ c.Check(formattedErr.Error(), Equals, `
+-----
+output line1
+output line2
+-----`)
+}
+
+func (ts *outputErrSuite) TestOutputErrNoOutput(c *C) {
+ err := fmt.Errorf("test error")
+ formattedErr := OutputErr([]byte{}, err)
+ c.Check(formattedErr, Equals, err)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "os"
+)
+
+// FileExists return true if given path can be stat()ed by us. Note that
+// it may return false on e.g. permission issues.
+func FileExists(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil
+}
+
+// IsDirectory return true if the given path can be stat()ed by us and
+// is a directory. Note that it may return false on e.g. permission issues.
+func IsDirectory(path string) bool {
+ fileInfo, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+
+ return fileInfo.IsDir()
+}
+
+// IsDevice checks if the given os.FileMode coresponds to a device (char/block)
+func IsDevice(mode os.FileMode) bool {
+ return (mode & (os.ModeDevice | os.ModeCharDevice)) != 0
+}
+
+// IsSymlink returns true if the given file is a symlink
+func IsSymlink(path string) bool {
+ fileInfo, err := os.Lstat(path)
+ if err != nil {
+ return false
+ }
+
+ return (fileInfo.Mode() & os.ModeSymlink) != 0
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+)
+
+type StatTestSuite struct{}
+
+var _ = Suite(&StatTestSuite{})
+
+func (ts *StatTestSuite) TestFileDoesNotExist(c *C) {
+ c.Assert(FileExists("/i-do-not-exist"), Equals, false)
+}
+
+func (ts *StatTestSuite) TestFileExistsSimple(c *C) {
+ fname := filepath.Join(c.MkDir(), "foo")
+ err := ioutil.WriteFile(fname, []byte(fname), 0644)
+ c.Assert(err, IsNil)
+
+ c.Assert(FileExists(fname), Equals, true)
+}
+
+func (ts *StatTestSuite) TestFileExistsExistsOddPermissions(c *C) {
+ fname := filepath.Join(c.MkDir(), "foo")
+ err := ioutil.WriteFile(fname, []byte(fname), 0100)
+ c.Assert(err, IsNil)
+
+ c.Assert(FileExists(fname), Equals, true)
+}
+
+func (ts *StatTestSuite) TestIsDirectoryDoesNotExist(c *C) {
+ c.Assert(IsDirectory("/i-do-not-exist"), Equals, false)
+}
+
+func (ts *StatTestSuite) TestIsDirectorySimple(c *C) {
+ dname := filepath.Join(c.MkDir(), "bar")
+ err := os.Mkdir(dname, 0700)
+ c.Assert(err, IsNil)
+
+ c.Assert(IsDirectory(dname), Equals, true)
+}
+
+func (ts *StatTestSuite) TestIsSymlink(c *C) {
+ sname := filepath.Join(c.MkDir(), "symlink")
+ err := os.Symlink("/", sname)
+ c.Assert(err, IsNil)
+
+ c.Assert(IsSymlink(sname), Equals, true)
+}
+
+func (ts *StatTestSuite) TestIsSymlinkNoSymlink(c *C) {
+ c.Assert(IsSymlink(c.MkDir()), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+)
+
+// FileState describes the expected content and meta data of a single file.
+type FileState struct {
+ Content []byte
+ Mode os.FileMode
+}
+
+// ErrSameState is returned when the state of a file has not changed.
+var ErrSameState = fmt.Errorf("file state has not changed")
+
+// EnsureDirState ensures that directory content matches expectations.
+//
+// EnsureDirState enumerates all the files in the specified directory that
+// match the provided pattern (glob). Each enumerated file is checked to ensure
+// that the contents, permissions are what is desired. Unexpected files are
+// removed. Missing files are created and differing files are corrected. Files
+// not matching the pattern are ignored.
+//
+// Note that EnsureDirState only checks for permissions and content. Other
+// security mechanisms, including file ownership and extended attributes are
+// *not* supported.
+//
+// The content map describes each of the files that are intended to exist in
+// the directory. Map keys must be file names relative to the directory.
+// Sub-directories in the name are not allowed.
+//
+// If writing any of the files fails, EnsureDirState switches to erase mode
+// where *all* of the files managed by the glob pattern are removed (including
+// those that may have been already written). The return value is an empty list
+// of changed files, the real list of removed files and the first error.
+//
+// If an error happens while removing files then such a file is not removed but
+// the removal continues until the set of managed files matching the glob is
+// exhausted.
+//
+// In all cases, the function returns the first error it has encountered.
+func EnsureDirState(dir, glob string, content map[string]*FileState) (changed, removed []string, err error) {
+ if _, err := filepath.Match(glob, "foo"); err != nil {
+ panic(fmt.Sprintf("EnsureDirState got invalid pattern %q: %s", glob, err))
+ }
+ for baseName := range content {
+ if filepath.Base(baseName) != baseName {
+ panic(fmt.Sprintf("EnsureDirState got filename %q which has a path component", baseName))
+ }
+ if ok, _ := filepath.Match(glob, baseName); !ok {
+ panic(fmt.Sprintf("EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, glob))
+ }
+ }
+ // Change phase (create/change files described by content)
+ var firstErr error
+ for baseName, fileState := range content {
+ filePath := filepath.Join(dir, baseName)
+ err := EnsureFileState(filePath, fileState)
+ if err == ErrSameState {
+ continue
+ }
+ if err != nil {
+ // On write failure, switch to erase mode. Desired content is set
+ // to nothing (no content) changed files are forgotten and the
+ // writing loop stops. The subsequent erase loop will remove all
+ // the managed content.
+ firstErr = err
+ content = nil
+ changed = nil
+ break
+ }
+ changed = append(changed, baseName)
+ }
+ // Delete phase (remove files matching the glob that are not in content)
+ matches, err := filepath.Glob(filepath.Join(dir, glob))
+ if err != nil {
+ sort.Strings(changed)
+ return changed, nil, err
+ }
+ for _, filePath := range matches {
+ baseName := filepath.Base(filePath)
+ if content[baseName] != nil {
+ continue
+ }
+ err := os.Remove(filePath)
+ if err != nil {
+ if firstErr == nil {
+ firstErr = err
+ }
+ continue
+ }
+ removed = append(removed, baseName)
+ }
+ sort.Strings(changed)
+ sort.Strings(removed)
+ return changed, removed, firstErr
+}
+
+// EnsureFileState ensures that the file is in the expected state. It will not attempt
+// to remove the file if no content is provided.
+func EnsureFileState(filePath string, fileState *FileState) error {
+ stat, err := os.Stat(filePath)
+ if os.IsNotExist(err) {
+ return AtomicWriteFile(filePath, fileState.Content, fileState.Mode, 0)
+ }
+ if err != nil {
+ return err
+ }
+ if stat.Mode().Perm() == fileState.Mode.Perm() && stat.Size() == int64(len(fileState.Content)) {
+ content, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ return err
+ }
+ if bytes.Equal(content, fileState.Content) {
+ // Return a special error if the file doesn't need to be changed
+ return ErrSameState
+ }
+ }
+ return AtomicWriteFile(filePath, fileState.Content, fileState.Mode, 0)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+type EnsureDirStateSuite struct {
+ dir string
+ glob string
+}
+
+var _ = Suite(&EnsureDirStateSuite{glob: "*.snap"})
+
+func (s *EnsureDirStateSuite) SetUpTest(c *C) {
+ s.dir = c.MkDir()
+}
+
+func (s *EnsureDirStateSuite) TestVerifiesExpectedFiles(c *C) {
+ name := filepath.Join(s.dir, "expected.snap")
+ err := ioutil.WriteFile(name, []byte("expected"), 0600)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{
+ "expected.snap": {Content: []byte("expected"), Mode: 0600},
+ })
+ c.Assert(err, IsNil)
+ // Report says that nothing has changed
+ c.Assert(changed, HasLen, 0)
+ c.Assert(removed, HasLen, 0)
+ // The content is correct
+ content, err := ioutil.ReadFile(path.Join(s.dir, "expected.snap"))
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte("expected"))
+ // The permissions are correct
+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureDirStateSuite) TestCreatesMissingFiles(c *C) {
+ name := filepath.Join(s.dir, "missing.snap")
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{
+ "missing.snap": {Content: []byte(`content`), Mode: 0600},
+ })
+ c.Assert(err, IsNil)
+ // Created file is reported
+ c.Assert(changed, DeepEquals, []string{"missing.snap"})
+ c.Assert(removed, HasLen, 0)
+ // The content is correct
+ content, err := ioutil.ReadFile(name)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte("content"))
+ // The permissions are correct
+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureDirStateSuite) TestRemovesUnexpectedFiless(c *C) {
+ name := filepath.Join(s.dir, "evil.snap")
+ err := ioutil.WriteFile(name, []byte(`evil text`), 0600)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{})
+ c.Assert(err, IsNil)
+ // Removed file is reported
+ c.Assert(changed, HasLen, 0)
+ c.Assert(removed, DeepEquals, []string{"evil.snap"})
+ // The file is removed
+ _, err = os.Stat(name)
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *EnsureDirStateSuite) TestIgnoresUnrelatedFiles(c *C) {
+ name := filepath.Join(s.dir, "unrelated")
+ err := ioutil.WriteFile(name, []byte(`text`), 0600)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{})
+ c.Assert(err, IsNil)
+ // Report says that nothing has changed
+ c.Assert(changed, HasLen, 0)
+ c.Assert(removed, HasLen, 0)
+ // The file is still there
+ _, err = os.Stat(name)
+ c.Assert(err, IsNil)
+}
+
+func (s *EnsureDirStateSuite) TestCorrectsFilesWithDifferentSize(c *C) {
+ name := filepath.Join(s.dir, "differing.snap")
+ err := ioutil.WriteFile(name, []byte(``), 0600)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{
+ "differing.snap": {Content: []byte(`Hello World`), Mode: 0600},
+ })
+ c.Assert(err, IsNil)
+ // changed file is reported
+ c.Assert(changed, DeepEquals, []string{"differing.snap"})
+ c.Assert(removed, HasLen, 0)
+ // The content is changed
+ content, err := ioutil.ReadFile(name)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte("Hello World"))
+ // The permissions are what we expect
+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureDirStateSuite) TestCorrectsFilesWithSameSize(c *C) {
+ name := filepath.Join(s.dir, "differing.snap")
+ err := ioutil.WriteFile(name, []byte("evil"), 0600)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{
+ "differing.snap": {Content: []byte("good"), Mode: 0600},
+ })
+ c.Assert(err, IsNil)
+ // changed file is reported
+ c.Assert(changed, DeepEquals, []string{"differing.snap"})
+ c.Assert(removed, HasLen, 0)
+ // The content is changed
+ content, err := ioutil.ReadFile(name)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte("good"))
+ // The permissions are what we expect
+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureDirStateSuite) TestFixesFilesWithBadPermissions(c *C) {
+ name := filepath.Join(s.dir, "sensitive.snap")
+ // NOTE: the existing file is currently wide-open for everyone"
+ err := ioutil.WriteFile(name, []byte("password"), 0666)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{
+ // NOTE: we want the file to be private
+ "sensitive.snap": {Content: []byte("password"), Mode: 0600},
+ })
+ c.Assert(err, IsNil)
+ // changed file is reported
+ c.Assert(changed, DeepEquals, []string{"sensitive.snap"})
+ c.Assert(removed, HasLen, 0)
+ // The content is still the same
+ content, err := ioutil.ReadFile(name)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte("password"))
+ // The permissions are changed
+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureDirStateSuite) TestReportsAbnormalFileLocation(c *C) {
+ c.Assert(func() {
+ osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{"subdir/file.snap": {}})
+ }, PanicMatches, `EnsureDirState got filename "subdir/file.snap" which has a path component`)
+}
+
+func (s *EnsureDirStateSuite) TestReportsAbnormalFileName(c *C) {
+ c.Assert(func() {
+ osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{"without-namespace": {}})
+ }, PanicMatches, `EnsureDirState got filename "without-namespace" which doesn't match the glob pattern "\*\.snap"`)
+}
+
+func (s *EnsureDirStateSuite) TestReportsAbnormalPatterns(c *C) {
+ c.Assert(func() { osutil.EnsureDirState(s.dir, "[", nil) },
+ PanicMatches, `EnsureDirState got invalid pattern "\[": syntax error in pattern`)
+}
+
+func (s *EnsureDirStateSuite) TestRemovesAllManagedFilesOnError(c *C) {
+ // Create a "prior.snap" file
+ prior := filepath.Join(s.dir, "prior.snap")
+ err := ioutil.WriteFile(prior, []byte("data"), 0600)
+ c.Assert(err, IsNil)
+ // Create a "clash.snap" directory to simulate failure
+ clash := filepath.Join(s.dir, "clash.snap")
+ err = os.Mkdir(clash, 0000)
+ c.Assert(err, IsNil)
+ // Try to ensure directory state
+ changed, removed, err := osutil.EnsureDirState(s.dir, s.glob, map[string]*osutil.FileState{
+ "prior.snap": {Content: []byte("data"), Mode: 0600},
+ "clash.snap": {Content: []byte("data"), Mode: 0600},
+ })
+ c.Assert(changed, HasLen, 0)
+ c.Assert(removed, DeepEquals, []string{"clash.snap", "prior.snap"})
+ c.Assert(err, ErrorMatches, "rename .* .*/clash.snap: is a directory")
+ // The clashing file is removed
+ _, err = os.Stat(clash)
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+var userLookup = user.Lookup
+
+var sudoersDotD = "/etc/sudoers.d"
+
+var sudoersTemplate = `
+# Created by snap create-user
+
+# User rules for %[1]s
+%[1]s ALL=(ALL) NOPASSWD:ALL
+`
+
+type AddUserOptions struct {
+ Sudoer bool
+ ExtraUsers bool
+ Gecos string
+ SSHKeys []string
+ // crypt(3) compatible password of the form $id$salt$hash
+ Password string
+}
+
+func AddUser(name string, opts *AddUserOptions) error {
+ if opts == nil {
+ opts = &AddUserOptions{}
+ }
+
+ // we check the (user)name ourselves, adduser is a bit too
+ // strict (i.e. no `.`) - this regexp is in sync with that SSO
+ // allows as valid usernames
+ validNames := regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`)
+ if !validNames.MatchString(name) {
+ return fmt.Errorf("cannot add user %q: name contains invalid characters", name)
+ }
+
+ cmdStr := []string{
+ "adduser",
+ "--force-badname",
+ "--gecos", opts.Gecos,
+ "--disabled-password",
+ }
+ if opts.ExtraUsers {
+ cmdStr = append(cmdStr, "--extrausers")
+ }
+ cmdStr = append(cmdStr, name)
+
+ cmd := exec.Command(cmdStr[0], cmdStr[1:]...)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("adduser failed with %s: %s", err, output)
+ }
+
+ if opts.Sudoer {
+ // Must escape "." as files containing it are ignored in sudoers.d.
+ sudoersFile := filepath.Join(sudoersDotD, "create-user-"+strings.Replace(name, ".", "%2E", -1))
+ if err := AtomicWriteFile(sudoersFile, []byte(fmt.Sprintf(sudoersTemplate, name)), 0400, 0); err != nil {
+ return fmt.Errorf("cannot create file under sudoers.d: %s", err)
+ }
+ }
+
+ if opts.Password != "" {
+ cmdStr := []string{
+ "usermod",
+ "--password", opts.Password,
+ // no --extrauser required, see LP: #1562872
+ name,
+ }
+ if output, err := exec.Command(cmdStr[0], cmdStr[1:]...).CombinedOutput(); err != nil {
+ return fmt.Errorf("setting password failed: %s", OutputErr(output, err))
+ }
+ }
+
+ u, err := userLookup(name)
+ if err != nil {
+ return fmt.Errorf("cannot find user %q: %s", name, err)
+ }
+
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ return fmt.Errorf("cannot parse user id %s: %s", u.Uid, err)
+ }
+ gid, err := strconv.Atoi(u.Gid)
+ if err != nil {
+ return fmt.Errorf("cannot parse group id %s: %s", u.Gid, err)
+ }
+
+ sshDir := filepath.Join(u.HomeDir, ".ssh")
+ if err := MkdirAllChown(sshDir, 0700, uid, gid); err != nil {
+ return fmt.Errorf("cannot create %s: %s", sshDir, err)
+ }
+ authKeys := filepath.Join(sshDir, "authorized_keys")
+ authKeysContent := strings.Join(opts.SSHKeys, "\n")
+ if err := AtomicWriteFileChown(authKeys, []byte(authKeysContent), 0600, 0, uid, gid); err != nil {
+ return fmt.Errorf("cannot write %s: %s", authKeys, err)
+ }
+
+ return nil
+}
+
+var userCurrent = user.Current
+
+// RealUser finds the user behind a sudo invocation when root, if applicable
+// and possible.
+//
+// Don't check SUDO_USER when not root and simply return the current uid
+// to properly support sudo'ing from root to a non-root user
+func RealUser() (*user.User, error) {
+ cur, err := userCurrent()
+ if err != nil {
+ return nil, err
+ }
+
+ // not root, so no sudo invocation we care about
+ if cur.Uid != "0" {
+ return cur, nil
+ }
+
+ realName := os.Getenv("SUDO_USER")
+ if realName == "" {
+ // not sudo; current is correct
+ return cur, nil
+ }
+
+ real, err := user.Lookup(realName)
+ // can happen when sudo is used to enter a chroot (e.g. pbuilder)
+ if _, ok := err.(user.UnknownUserError); ok {
+ return cur, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return real, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil_test
+
+import (
+ "io/ioutil"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strconv"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type createUserSuite struct {
+ testutil.BaseTest
+
+ mockHome string
+ restorer func()
+
+ mockAddUser *testutil.MockCmd
+ mockUserMod *testutil.MockCmd
+}
+
+var _ = check.Suite(&createUserSuite{})
+
+func (s *createUserSuite) SetUpTest(c *check.C) {
+ s.mockHome = c.MkDir()
+ s.restorer = osutil.MockUserLookup(func(string) (*user.User, error) {
+ current, err := user.Current()
+ if err != nil {
+ c.Fatalf("user.Current() failed with %s", err)
+ }
+ return &user.User{
+ HomeDir: s.mockHome,
+ Gid: current.Gid,
+ Uid: current.Uid,
+ }, nil
+ })
+ s.mockAddUser = testutil.MockCommand(c, "adduser", "")
+ s.mockUserMod = testutil.MockCommand(c, "usermod", "")
+}
+
+func (s *createUserSuite) TearDownTest(c *check.C) {
+ s.restorer()
+ s.mockAddUser.Restore()
+}
+
+func (s *createUserSuite) TestAddUserExtraUsersFalse(c *check.C) {
+ err := osutil.AddUser("lakatos", &osutil.AddUserOptions{
+ Gecos: "my gecos",
+ ExtraUsers: false,
+ })
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.mockAddUser.Calls(), check.DeepEquals, [][]string{
+ {"adduser", "--force-badname", "--gecos", "my gecos", "--disabled-password", "lakatos"},
+ })
+}
+
+func (s *createUserSuite) TestAddUserExtraUsersTrue(c *check.C) {
+ err := osutil.AddUser("lakatos", &osutil.AddUserOptions{
+ Gecos: "my gecos",
+ ExtraUsers: true,
+ })
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.mockAddUser.Calls(), check.DeepEquals, [][]string{
+ {"adduser", "--force-badname", "--gecos", "my gecos", "--disabled-password", "--extrausers", "lakatos"},
+ })
+}
+
+func (s *createUserSuite) TestAddSudoUser(c *check.C) {
+ mockSudoers := c.MkDir()
+ restorer := osutil.MockSudoersDotD(mockSudoers)
+ defer restorer()
+
+ err := osutil.AddUser("karl.sagan", &osutil.AddUserOptions{
+ Gecos: "my gecos",
+ Sudoer: true,
+ ExtraUsers: true,
+ })
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.mockAddUser.Calls(), check.DeepEquals, [][]string{
+ {"adduser", "--force-badname", "--gecos", "my gecos", "--disabled-password", "--extrausers", "karl.sagan"},
+ })
+
+ fs, _ := filepath.Glob(filepath.Join(mockSudoers, "*"))
+ c.Assert(fs, check.HasLen, 1)
+ c.Assert(filepath.Base(fs[0]), check.Equals, "create-user-karl%2Esagan")
+ bs, err := ioutil.ReadFile(fs[0])
+ c.Assert(err, check.IsNil)
+ c.Check(string(bs), check.Equals, `
+# Created by snap create-user
+
+# User rules for karl.sagan
+karl.sagan ALL=(ALL) NOPASSWD:ALL
+`)
+}
+
+func (s *createUserSuite) TestAddUserSSHKeys(c *check.C) {
+ err := osutil.AddUser("karl.sagan", &osutil.AddUserOptions{
+ SSHKeys: []string{"ssh-key1", "ssh-key2"},
+ })
+ c.Assert(err, check.IsNil)
+ sshKeys, err := ioutil.ReadFile(filepath.Join(s.mockHome, ".ssh", "authorized_keys"))
+ c.Assert(err, check.IsNil)
+ c.Check(string(sshKeys), check.Equals, "ssh-key1\nssh-key2")
+
+}
+
+func (s *createUserSuite) TestAddUserInvalidUsername(c *check.C) {
+ err := osutil.AddUser("k!", nil)
+ c.Assert(err, check.ErrorMatches, `cannot add user "k!": name contains invalid characters`)
+}
+
+func (s *createUserSuite) TestAddUserWithPassword(c *check.C) {
+ mockSudoers := c.MkDir()
+ restorer := osutil.MockSudoersDotD(mockSudoers)
+ defer restorer()
+
+ err := osutil.AddUser("karl.sagan", &osutil.AddUserOptions{
+ Gecos: "my gecos",
+ Password: "$6$salt$hash",
+ })
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.mockAddUser.Calls(), check.DeepEquals, [][]string{
+ {"adduser", "--force-badname", "--gecos", "my gecos", "--disabled-password", "karl.sagan"},
+ })
+ c.Check(s.mockUserMod.Calls(), check.DeepEquals, [][]string{
+ {"usermod", "--password", "$6$salt$hash", "karl.sagan"},
+ })
+
+}
+
+func (s *createUserSuite) TestRealUser(c *check.C) {
+ oldUser := os.Getenv("SUDO_USER")
+ defer func() { os.Setenv("SUDO_USER", oldUser) }()
+
+ for _, t := range []struct {
+ SudoUsername string
+ CurrentUsername string
+ CurrentUid int
+ }{
+ // simulate regular "root", no SUDO_USER set
+ {"", os.Getenv("USER"), 0},
+ // simulate a normal sudo invocation
+ {"guy", "guy", 0},
+ // simulate running "sudo -u some-user -i" as root
+ // (LP: #1638656)
+ {"root", os.Getenv("USER"), 1000},
+ } {
+ restore := osutil.MockUserCurrent(func() (*user.User, error) {
+ return &user.User{
+ Username: t.CurrentUsername,
+ Uid: strconv.Itoa(t.CurrentUid),
+ }, nil
+ })
+ defer restore()
+
+ os.Setenv("SUDO_USER", t.SudoUsername)
+ cur, err := osutil.RealUser()
+ c.Assert(err, check.IsNil)
+ c.Check(cur.Username, check.Equals, t.CurrentUsername)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package osutil
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// Winsize is from tty_ioctl(4)
+type Winsize struct {
+ Row uint16
+ Col uint16
+ xpixel uint16 // unused
+ Ypixel uint16 // unused
+}
+
+// GetTermWinsize performs the TIOCGWINSZ ioctl on stdout
+func GetTermWinsize() (*Winsize, error) {
+ ws := &Winsize{}
+ x, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws)))
+
+ if int(x) == -1 {
+ // returning ws on error lets people that don't care
+ // about the error get on with querying the struct
+ // (which will be empty on error).
+ return ws, errno
+ }
+
+ return ws, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package assertstate implements the manager and state aspects responsible
+// for the enforcement of assertions in the system and manages the system-wide
+// assertion database.
+package assertstate
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+// AssertManager is responsible for the enforcement of assertions in
+// system states. It manipulates the observed system state to ensure
+// nothing in it violates existing assertions, or misses required
+// ones.
+type AssertManager struct {
+ runner *state.TaskRunner
+}
+
+// Manager returns a new assertion manager.
+func Manager(s *state.State) (*AssertManager, error) {
+ runner := state.NewTaskRunner(s)
+
+ runner.AddHandler("validate-snap", doValidateSnap, nil)
+
+ db, err := sysdb.Open()
+ if err != nil {
+ return nil, err
+ }
+
+ s.Lock()
+ ReplaceDB(s, db)
+ s.Unlock()
+
+ return &AssertManager{runner: runner}, nil
+}
+
+// Ensure implements StateManager.Ensure.
+func (m *AssertManager) Ensure() error {
+ m.runner.Ensure()
+ return nil
+}
+
+// Wait implements StateManager.Wait.
+func (m *AssertManager) Wait() {
+ m.runner.Wait()
+}
+
+// Stop implements StateManager.Stop.
+func (m *AssertManager) Stop() {
+ m.runner.Stop()
+}
+
+type cachedDBKey struct{}
+
+// ReplaceDB replaces the assertion database used by the manager.
+func ReplaceDB(state *state.State, db *asserts.Database) {
+ state.Cache(cachedDBKey{}, db)
+}
+
+func cachedDB(s *state.State) *asserts.Database {
+ db := s.Cached(cachedDBKey{})
+ if db == nil {
+ panic("internal error: needing an assertion database before the assertion manager is initialized")
+ }
+ return db.(*asserts.Database)
+}
+
+// DB returns a read-only view of system assertion database.
+func DB(s *state.State) asserts.RODatabase {
+ return cachedDB(s)
+}
+
+// Add the given assertion to the system assertion database.
+func Add(s *state.State, a asserts.Assertion) error {
+ // TODO: deal together with asserts itself with (cascading) side effects of possible assertion updates
+ return cachedDB(s).Add(a)
+}
+
+// Batch allows to accumulate a set of assertions possibly out of prerequisite order and then add them in one go to the system assertion database.
+type Batch struct {
+ bs asserts.Backstore
+ refs []*asserts.Ref
+}
+
+// NewBatch creates a new Batch to accumulate assertions to add in one go to the system assertion database.
+func NewBatch() *Batch {
+ return &Batch{
+ bs: asserts.NewMemoryBackstore(),
+ refs: nil,
+ }
+}
+
+// Add one assertion to the batch.
+func (b *Batch) Add(a asserts.Assertion) error {
+ if !a.SupportedFormat() {
+ return &asserts.UnsupportedFormatError{Ref: a.Ref(), Format: a.Format()}
+ }
+ if err := b.bs.Put(a.Type(), a); err != nil {
+ if revErr, ok := err.(*asserts.RevisionError); ok {
+ if revErr.Current >= a.Revision() {
+ // we already got something more recent
+ return nil
+ }
+ }
+ return err
+ }
+ b.refs = append(b.refs, a.Ref())
+ return nil
+}
+
+// AddStream adds a stream of assertions to the batch.
+// Returns references to to the assertions effectively added.
+func (b *Batch) AddStream(r io.Reader) ([]*asserts.Ref, error) {
+ start := len(b.refs)
+ dec := asserts.NewDecoder(r)
+ for {
+ a, err := dec.Decode()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if err := b.Add(a); err != nil {
+ return nil, err
+ }
+ }
+ added := b.refs[start:]
+ if len(added) == 0 {
+ return nil, nil
+ }
+ refs := make([]*asserts.Ref, len(added))
+ copy(refs, added)
+ return refs, nil
+}
+
+// Commit adds the batch of assertions to the system assertion database.
+func (b *Batch) Commit(st *state.State) error {
+ db := cachedDB(st)
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ a, err := b.bs.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat())
+ if err == asserts.ErrNotFound {
+ // fallback to pre-existing assertions
+ a, err = ref.Resolve(db.Find)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("cannot find %s: %s", ref, err)
+ }
+ return a, nil
+ }
+
+ // linearize using fetcher
+ f := newFetcher(st, retrieve)
+ for _, ref := range b.refs {
+ if err := f.Fetch(ref); err != nil {
+ return err
+ }
+ }
+
+ // TODO: trigger w. caller a global sanity check if something is revoked
+ // (but try to save as much possible still),
+ // or err is a check error
+ return f.commit()
+}
+
+// TODO: snapstate also has this, move to auth, or change a bit the approach now that we have AuthContext in the store?
+func userFromUserID(st *state.State, userID int) (*auth.UserState, error) {
+ if userID == 0 {
+ return nil, nil
+ }
+ return auth.User(st, userID)
+}
+
+type fetcher struct {
+ db *asserts.Database
+ asserts.Fetcher
+ fetched []asserts.Assertion
+}
+
+// newFetches creates a fetcher used to retrieve assertions and later commit them to the system database in one go.
+func newFetcher(s *state.State, retrieve func(*asserts.Ref) (asserts.Assertion, error)) *fetcher {
+ db := cachedDB(s)
+
+ f := &fetcher{db: db}
+
+ save := func(a asserts.Assertion) error {
+ f.fetched = append(f.fetched, a)
+ return nil
+ }
+
+ f.Fetcher = asserts.NewFetcher(db, retrieve, save)
+
+ return f
+}
+
+type commitError struct {
+ errs []error
+}
+
+func (e *commitError) Error() string {
+ l := []string{""}
+ for _, e := range e.errs {
+ l = append(l, e.Error())
+ }
+ return fmt.Sprintf("cannot add some assertions to the system database:%s", strings.Join(l, "\n - "))
+}
+
+// commit does a best effort of adding all the fetched assertions to the system database.
+func (f *fetcher) commit() error {
+ var errs []error
+ for _, a := range f.fetched {
+ err := f.db.Add(a)
+ if asserts.IsUnaccceptedUpdate(err) {
+ if _, ok := err.(*asserts.UnsupportedFormatError); ok {
+ // we kept the old one, but log the issue
+ logger.Noticef("Cannot update assertion: %v", err)
+ }
+ // be idempotent
+ // system db has already the same or newer
+ continue
+ }
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) != 0 {
+ return &commitError{errs: errs}
+ }
+ return nil
+}
+
+func doFetch(s *state.State, userID int, fetching func(asserts.Fetcher) error) error {
+ // TODO: once we have a bulk assertion retrieval endpoint this approach will change
+
+ user, err := userFromUserID(s, userID)
+ if err != nil {
+ return err
+ }
+
+ sto := snapstate.Store(s)
+
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ // TODO: ignore errors if already in db?
+ return sto.Assertion(ref.Type, ref.PrimaryKey, user)
+ }
+
+ f := newFetcher(s, retrieve)
+
+ s.Unlock()
+ err = fetching(f)
+ s.Lock()
+ if err != nil {
+ return err
+ }
+
+ // TODO: trigger w. caller a global sanity check if a is revoked
+ // (but try to save as much possible still),
+ // or err is a check error
+ return f.commit()
+}
+
+// doValidateSnap fetches the relevant assertions for the snap being installed and cross checks them with the snap.
+func doValidateSnap(t *state.Task, _ *tomb.Tomb) error {
+ t.State().Lock()
+ defer t.State().Unlock()
+
+ snapsup, err := snapstate.TaskSnapSetup(t)
+ if err != nil {
+ return nil
+ }
+
+ sha3_384, snapSize, err := asserts.SnapFileSHA3_384(snapsup.SnapPath)
+ if err != nil {
+ return err
+ }
+
+ err = doFetch(t.State(), snapsup.UserID, func(f asserts.Fetcher) error {
+ return snapasserts.FetchSnapAssertions(f, sha3_384)
+ })
+ if notFound, ok := err.(*store.AssertionNotFoundError); ok {
+ if notFound.Ref.Type == asserts.SnapRevisionType {
+ return fmt.Errorf("cannot verify snap %q, no matching signatures found", snapsup.Name())
+ } else {
+ return fmt.Errorf("cannot find supported signatures to verify snap %q and its hash (%v)", snapsup.Name(), notFound)
+ }
+ }
+ if err != nil {
+ return err
+ }
+
+ db := DB(t.State())
+ err = snapasserts.CrossCheck(snapsup.Name(), sha3_384, snapSize, snapsup.SideInfo, db)
+ if err != nil {
+ // TODO: trigger a global sanity check
+ // that will generate the changes to deal with this
+ // for things like snap-decl revocation and renames?
+ return err
+ }
+
+ // TODO: set DeveloperID from assertions
+ return nil
+}
+
+// RefreshSnapDeclarations refetches all the current snap declarations and their prerequisites.
+func RefreshSnapDeclarations(s *state.State, userID int) error {
+ snapStates, err := snapstate.All(s)
+ if err != nil {
+ return nil
+ }
+ fetching := func(f asserts.Fetcher) error {
+ for _, snapst := range snapStates {
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ if info.SnapID == "" {
+ continue
+ }
+ if err := snapasserts.FetchSnapDeclaration(f, info.SnapID); err != nil {
+ return fmt.Errorf("cannot refresh snap-declaration for %q: %v", info.Name(), err)
+ }
+ }
+ return nil
+ }
+ return doFetch(s, userID, fetching)
+}
+
+type refreshControlError struct {
+ errs []error
+}
+
+func (e *refreshControlError) Error() string {
+ if len(e.errs) == 1 {
+ return e.errs[0].Error()
+ }
+ l := []string{""}
+ for _, e := range e.errs {
+ l = append(l, e.Error())
+ }
+ return fmt.Sprintf("refresh control errors:%s", strings.Join(l, "\n - "))
+}
+
+// ValidateRefreshes validates the refresh candidate revisions represented by the snapInfos, looking for the needed refresh control validation assertions, it returns a validated subset in validated and a summary error if not all candidates validated.
+func ValidateRefreshes(s *state.State, snapInfos []*snap.Info, userID int) (validated []*snap.Info, err error) {
+ // maps gated snap-ids to gating snap-ids
+ controlled := make(map[string][]string)
+ // maps gating snap-ids to their snap names
+ gatingNames := make(map[string]string)
+
+ db := DB(s)
+ snapStates, err := snapstate.All(s)
+ if err != nil {
+ return nil, err
+ }
+ for snapName, snapst := range snapStates {
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return nil, err
+ }
+ if info.SnapID == "" {
+ continue
+ }
+ gatingID := info.SnapID
+ a, err := db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": release.Series,
+ "snap-id": gatingID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot find snap declaration for installed snap %q (id %q): err", snapName, gatingID)
+ }
+ decl := a.(*asserts.SnapDeclaration)
+ control := decl.RefreshControl()
+ if len(control) == 0 {
+ continue
+ }
+ gatingNames[gatingID] = decl.SnapName()
+ for _, gatedID := range control {
+ controlled[gatedID] = append(controlled[gatedID], gatingID)
+ }
+ }
+
+ var errs []error
+ for _, candInfo := range snapInfos {
+ gatedID := candInfo.SnapID
+ gating := controlled[gatedID]
+ if len(gating) == 0 { // easy case, no refresh control
+ validated = append(validated, candInfo)
+ continue
+ }
+
+ var validationRefs []*asserts.Ref
+
+ fetching := func(f asserts.Fetcher) error {
+ for _, gatingID := range gating {
+ valref := &asserts.Ref{
+ Type: asserts.ValidationType,
+ PrimaryKey: []string{release.Series, gatingID, gatedID, candInfo.Revision.String()},
+ }
+ err := f.Fetch(valref)
+ if notFound, ok := err.(*store.AssertionNotFoundError); ok && notFound.Ref.Type == asserts.ValidationType {
+ return fmt.Errorf("no validation by %q", gatingNames[gatingID])
+ }
+ if err != nil {
+ return fmt.Errorf("cannot find validation by %q: %v", gatingNames[gatingID], err)
+ }
+ validationRefs = append(validationRefs, valref)
+ }
+ return nil
+ }
+ err := doFetch(s, userID, fetching)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("cannot refresh %q to revision %s: %v", candInfo.Name(), candInfo.Revision, err))
+ continue
+ }
+
+ var revoked *asserts.Validation
+ for _, valref := range validationRefs {
+ a, err := valref.Resolve(db.Find)
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot find just fetched %v: %v", valref, err)
+ }
+ if val := a.(*asserts.Validation); val.Revoked() {
+ revoked = val
+ break
+ }
+ }
+ if revoked != nil {
+ errs = append(errs, fmt.Errorf("cannot refresh %q to revision %s: validation by %q (id %q) revoked", candInfo.Name(), candInfo.Revision, gatingNames[revoked.SnapID()], revoked.SnapID()))
+ continue
+ }
+
+ validated = append(validated, candInfo)
+ }
+
+ if errs != nil {
+ return validated, &refreshControlError{errs}
+ }
+
+ return validated, nil
+}
+
+func init() {
+ // hook validation of refreshes into snapstate logic
+ snapstate.ValidateRefreshes = ValidateRefreshes
+}
+
+// BaseDeclaration returns the base-declaration assertion with policies governing all snaps.
+func BaseDeclaration(s *state.State) (*asserts.BaseDeclaration, error) {
+ // TODO: switch keeping this in the DB and have it revisioned/updated
+ // via the store
+ baseDecl := asserts.BuiltinBaseDeclaration()
+ if baseDecl == nil {
+ return nil, asserts.ErrNotFound
+ }
+ return baseDecl, nil
+}
+
+// SnapDeclaration returns the snap-declaration for the given snap-id if it is present in the system assertion database.
+func SnapDeclaration(s *state.State, snapID string) (*asserts.SnapDeclaration, error) {
+ db := DB(s)
+ a, err := db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": release.Series,
+ "snap-id": snapID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return a.(*asserts.SnapDeclaration), nil
+}
+
+// Publisher returns the account assertion for publisher of the given snap-id if it is present in the system assertion database.
+func Publisher(s *state.State, snapID string) (*asserts.Account, error) {
+ db := DB(s)
+ a, err := db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": release.Series,
+ "snap-id": snapID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ snapDecl := a.(*asserts.SnapDeclaration)
+ a, err = db.Find(asserts.AccountType, map[string]string{
+ "account-id": snapDecl.PublisherID(),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot find account assertion for the publisher of snap %q: %v", snapDecl.SnapName(), err)
+ }
+ return a.(*asserts.Account), nil
+}
+
+// AutoAliases returns the auto-aliases list for the given installed snap.
+func AutoAliases(s *state.State, info *snap.Info) ([]string, error) {
+ if info.SnapID == "" {
+ // without declaration
+ return nil, nil
+ }
+ decl, err := SnapDeclaration(s, info.SnapID)
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot find snap-declaration for installed snap %q: %v", info.Name(), err)
+ }
+ return decl.AutoAliases(), nil
+}
+
+func init() {
+ // hook retrieving auto-aliases into snapstate logic
+ snapstate.AutoAliases = AutoAliases
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package assertstate_test
+
+import (
+ "bytes"
+ "crypto"
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "golang.org/x/crypto/sha3"
+ "golang.org/x/net/context"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+func TestAssertManager(t *testing.T) { TestingT(t) }
+
+type assertMgrSuite struct {
+ state *state.State
+ mgr *assertstate.AssertManager
+
+ storeSigning *assertstest.StoreStack
+ dev1Acct *asserts.Account
+ dev1Signing *assertstest.SigningDB
+
+ restore func()
+}
+
+var _ = Suite(&assertMgrSuite{})
+
+type fakeStore struct {
+ state *state.State
+ db asserts.RODatabase
+}
+
+func (sto *fakeStore) pokeStateLock() {
+ // the store should be called without the state lock held. Try
+ // to acquire it.
+ sto.state.Lock()
+ sto.state.Unlock()
+}
+
+func (sto *fakeStore) Assertion(assertType *asserts.AssertionType, key []string, _ *auth.UserState) (asserts.Assertion, error) {
+ sto.pokeStateLock()
+ ref := &asserts.Ref{Type: assertType, PrimaryKey: key}
+ a, err := ref.Resolve(sto.db.Find)
+ if err != nil {
+ return nil, &store.AssertionNotFoundError{Ref: ref}
+ }
+ return a, nil
+}
+
+func (*fakeStore) SnapInfo(store.SnapSpec, *auth.UserState) (*snap.Info, error) {
+ panic("fakeStore.SnapInfo not expected")
+}
+
+func (sto *fakeStore) Find(*store.Search, *auth.UserState) ([]*snap.Info, error) {
+ panic("fakeStore.Find not expected")
+}
+
+func (sto *fakeStore) ListRefresh([]*store.RefreshCandidate, *auth.UserState) ([]*snap.Info, error) {
+ panic("fakeStore.ListRefresh not expected")
+}
+
+func (sto *fakeStore) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error {
+ panic("fakeStore.Download not expected")
+}
+
+func (sto *fakeStore) SuggestedCurrency() string {
+ panic("fakeStore.SuggestedCurrency not expected")
+}
+
+func (sto *fakeStore) Buy(*store.BuyOptions, *auth.UserState) (*store.BuyResult, error) {
+ panic("fakeStore.Buy not expected")
+}
+
+func (sto *fakeStore) ReadyToBuy(*auth.UserState) error {
+ panic("fakeStore.ReadyToBuy not expected")
+}
+
+func (sto *fakeStore) Sections(*auth.UserState) ([]string, error) {
+ panic("fakeStore.Sections not expected")
+}
+
+func (s *assertMgrSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+ s.restore = sysdb.InjectTrusted(s.storeSigning.Trusted)
+
+ dev1PrivKey, _ := assertstest.GenerateKey(752)
+ s.dev1Acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ err := s.storeSigning.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ // developer signing
+ dev1AcctKey := assertstest.NewAccountKey(s.storeSigning, s.dev1Acct, nil, dev1PrivKey.PublicKey(), "")
+ err = s.storeSigning.Add(dev1AcctKey)
+ c.Assert(err, IsNil)
+
+ s.dev1Signing = assertstest.NewSigningDB(s.dev1Acct.AccountID(), dev1PrivKey)
+
+ s.state = state.New(nil)
+ mgr, err := assertstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.mgr = mgr
+
+ s.state.Lock()
+ snapstate.ReplaceStore(s.state, &fakeStore{
+ state: s.state,
+ db: s.storeSigning,
+ })
+ s.state.Unlock()
+}
+
+func (s *assertMgrSuite) TearDownTest(c *C) {
+ s.restore()
+}
+
+func (s *assertMgrSuite) TestDB(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ db := assertstate.DB(s.state)
+ c.Check(db, FitsTypeOf, (*asserts.Database)(nil))
+}
+
+func (s *assertMgrSuite) TestAdd(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // prereq store key
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ db := assertstate.DB(s.state)
+ devAcct, err := db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *assertMgrSuite) TestBatchAddStream(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ b := &bytes.Buffer{}
+ enc := asserts.NewEncoder(b)
+ // wrong order is ok
+ err := enc.Encode(s.dev1Acct)
+ c.Assert(err, IsNil)
+ enc.Encode(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := assertstate.NewBatch()
+ refs, err := batch.AddStream(b)
+ c.Assert(err, IsNil)
+ c.Check(refs, DeepEquals, []*asserts.Ref{
+ {Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
+ {Type: asserts.AccountKeyType, PrimaryKey: []string{s.storeSigning.StoreAccountKey("").PublicKeyID()}},
+ })
+
+ // noop
+ err = batch.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ err = batch.Commit(s.state)
+ c.Assert(err, IsNil)
+
+ db := assertstate.DB(s.state)
+ devAcct, err := db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *assertMgrSuite) TestBatchConsiderPreexisting(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // prereq store key
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := assertstate.NewBatch()
+ err = batch.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ err = batch.Commit(s.state)
+ c.Assert(err, IsNil)
+
+ db := assertstate.DB(s.state)
+ devAcct, err := db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *assertMgrSuite) TestBatchAddStreamReturnsEffectivelyAddedRefs(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ b := &bytes.Buffer{}
+ enc := asserts.NewEncoder(b)
+ // wrong order is ok
+ err := enc.Encode(s.dev1Acct)
+ c.Assert(err, IsNil)
+ enc.Encode(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := assertstate.NewBatch()
+
+ err = batch.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ refs, err := batch.AddStream(b)
+ c.Assert(err, IsNil)
+ c.Check(refs, DeepEquals, []*asserts.Ref{
+ {Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
+ })
+
+ err = batch.Commit(s.state)
+ c.Assert(err, IsNil)
+
+ db := assertstate.DB(s.state)
+ devAcct, err := db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *assertMgrSuite) TestBatchAddUnsupported(c *C) {
+ batch := assertstate.NewBatch()
+
+ var a asserts.Assertion
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+ headers := map[string]interface{}{
+ "format": "999",
+ "revision": "1",
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ var err error
+ a, err = s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ })()
+
+ err := batch.Add(a)
+ c.Check(err, ErrorMatches, `proposed "snap-declaration" assertion has format 999 but 1 is latest supported`)
+}
+
+func fakeSnap(rev int) []byte {
+ fake := fmt.Sprintf("hsqs________________%d", rev)
+ return []byte(fake)
+}
+
+func fakeHash(rev int) []byte {
+ h := sha3.Sum384(fakeSnap(rev))
+ return h[:]
+}
+
+func makeDigest(rev int) string {
+ d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev))
+ if err != nil {
+ panic(err)
+ }
+ return string(d)
+}
+
+func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) {
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDecl)
+ c.Assert(err, IsNil)
+
+ for _, rev := range revisions {
+ headers = map[string]interface{}{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": makeDigest(rev),
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapRev)
+ c.Assert(err, IsNil)
+ }
+}
+
+func (s *assertMgrSuite) TestDoFetch(c *C) {
+ s.prereqSnapAssertions(c, 10)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ref := &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{makeDigest(10)},
+ }
+
+ err := assertstate.DoFetch(s.state, 0, func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ })
+ c.Assert(err, IsNil)
+
+ snapRev, err := ref.Resolve(assertstate.DB(s.state).Find)
+ c.Assert(err, IsNil)
+ c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10)
+}
+
+func (s *assertMgrSuite) TestFetchIdempotent(c *C) {
+ s.prereqSnapAssertions(c, 10, 11)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ref := &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{makeDigest(10)},
+ }
+ fetching := func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ }
+
+ err := assertstate.DoFetch(s.state, 0, fetching)
+ c.Assert(err, IsNil)
+
+ ref = &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{makeDigest(11)},
+ }
+
+ err = assertstate.DoFetch(s.state, 0, fetching)
+ c.Assert(err, IsNil)
+
+ snapRev, err := ref.Resolve(assertstate.DB(s.state).Find)
+ c.Assert(err, IsNil)
+ c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 11)
+}
+
+func (s *assertMgrSuite) settle() {
+ // XXX: would like to use Overlord.Settle but not enough control there
+ for i := 0; i < 50; i++ {
+ s.mgr.Ensure()
+ s.mgr.Wait()
+ }
+}
+
+func (s *assertMgrSuite) TestValidateSnap(c *C) {
+ s.prereqSnapAssertions(c, 10)
+
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "foo.snap")
+ err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644)
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "...")
+ t := s.state.NewTask("validate-snap", "Fetch and check snap assertions")
+ snapsup := snapstate.SnapSetup{
+ SnapPath: snapPath,
+ UserID: 0,
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ SnapID: "snap-id-1",
+ Revision: snap.R(10),
+ },
+ }
+ t.Set("snap-setup", snapsup)
+ chg.AddTask(t)
+
+ s.state.Unlock()
+ defer s.mgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Err(), IsNil)
+
+ snapRev, err := assertstate.DB(s.state).Find(asserts.SnapRevisionType, map[string]string{
+ "snap-id": "snap-id-1",
+ "snap-sha3-384": makeDigest(10),
+ })
+ c.Assert(err, IsNil)
+ c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10)
+}
+
+func (s *assertMgrSuite) TestValidateSnapNotFound(c *C) {
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "foo.snap")
+ err := ioutil.WriteFile(snapPath, fakeSnap(33), 0644)
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "...")
+ t := s.state.NewTask("validate-snap", "Fetch and check snap assertions")
+ snapsup := snapstate.SnapSetup{
+ SnapPath: snapPath,
+ UserID: 0,
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ SnapID: "snap-id-1",
+ Revision: snap.R(33),
+ },
+ }
+ t.Set("snap-setup", snapsup)
+ chg.AddTask(t)
+
+ s.state.Unlock()
+ defer s.mgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Err(), ErrorMatches, `(?s).*cannot verify snap "foo", no matching signatures found.*`)
+}
+
+func (s *assertMgrSuite) TestValidateSnapCrossCheckFail(c *C) {
+ s.prereqSnapAssertions(c, 10)
+
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "foo.snap")
+ err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644)
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "...")
+ t := s.state.NewTask("validate-snap", "Fetch and check snap assertions")
+ snapsup := snapstate.SnapSetup{
+ SnapPath: snapPath,
+ UserID: 0,
+ SideInfo: &snap.SideInfo{
+ RealName: "f",
+ SnapID: "snap-id-1",
+ Revision: snap.R(10),
+ },
+ }
+ t.Set("snap-setup", snapsup)
+ chg.AddTask(t)
+
+ s.state.Unlock()
+ defer s.mgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Err(), ErrorMatches, `(?s).*cannot install snap "f" that is undergoing a rename to "foo".*`)
+}
+
+func (s *assertMgrSuite) TestValidateSnapSnapDeclIsTooNewFirstInstall(c *C) {
+ c.Skip("the assertion service will make this scenario not possible")
+
+ s.prereqSnapAssertions(c, 10)
+
+ tempdir := c.MkDir()
+ snapPath := filepath.Join(tempdir, "foo.snap")
+ err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644)
+ c.Assert(err, IsNil)
+
+ // update snap decl with one that is too new
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+ headers := map[string]interface{}{
+ "format": "999",
+ "revision": "1",
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDecl)
+ c.Assert(err, IsNil)
+ })()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "...")
+ t := s.state.NewTask("validate-snap", "Fetch and check snap assertions")
+ snapsup := snapstate.SnapSetup{
+ SnapPath: snapPath,
+ UserID: 0,
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ SnapID: "snap-id-1",
+ Revision: snap.R(10),
+ },
+ }
+ t.Set("snap-setup", snapsup)
+ chg.AddTask(t)
+
+ s.state.Unlock()
+ defer s.mgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Err(), ErrorMatches, `(?s).*proposed "snap-declaration" assertion has format 999 but 0 is latest supported.*`)
+}
+
+func (s *assertMgrSuite) snapDecl(c *C, name string, extraHeaders map[string]interface{}) *asserts.SnapDeclaration {
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": name + "-id",
+ "snap-name": name,
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for h, v := range extraHeaders {
+ headers[h] = v
+ }
+ decl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(decl)
+ c.Assert(err, IsNil)
+ return decl.(*asserts.SnapDeclaration)
+}
+
+func (s *assertMgrSuite) stateFromDecl(decl *asserts.SnapDeclaration, revno snap.Revision) {
+ name := decl.SnapName()
+ snapID := decl.SnapID()
+ snapstate.Set(s.state, name, &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: name, SnapID: snapID, Revision: revno},
+ },
+ Current: revno,
+ })
+}
+
+func (s *assertMgrSuite) TestRefreshSnapDeclarations(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ snapDeclBar := s.snapDecl(c, "bar", nil)
+
+ s.stateFromDecl(snapDeclFoo, snap.R(7))
+ s.stateFromDecl(snapDeclBar, snap.R(3))
+ snapstate.Set(s.state, "local", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{
+ {RealName: "local", Revision: snap.R(-1)},
+ },
+ Current: snap.R(-1),
+ })
+
+ // previous state
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBar)
+ c.Assert(err, IsNil)
+
+ // one changed assertion
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "foo-id",
+ "snap-name": "fo-o",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ "revision": "1",
+ }
+ snapDeclFoo1, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDeclFoo1)
+ c.Assert(err, IsNil)
+
+ err = assertstate.RefreshSnapDeclarations(s.state, 0)
+ c.Assert(err, IsNil)
+
+ a, err := assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(err, IsNil)
+ c.Check(a.(*asserts.SnapDeclaration).SnapName(), Equals, "fo-o")
+
+ // another one
+ // one changed assertion
+ headers = s.dev1Acct.Headers()
+ headers["display-name"] = "Dev 1 edited display-name"
+ headers["revision"] = "1"
+ dev1Acct1, err := s.storeSigning.Sign(asserts.AccountType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(dev1Acct1)
+ c.Assert(err, IsNil)
+
+ err = assertstate.RefreshSnapDeclarations(s.state, 0)
+ c.Assert(err, IsNil)
+
+ a, err = assertstate.DB(s.state).Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(a.(*asserts.Account).DisplayName(), Equals, "Dev 1 edited display-name")
+
+ // change snap decl to something that has a too new format
+
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+
+ headers := map[string]interface{}{
+ "format": "999",
+ "series": "16",
+ "snap-id": "foo-id",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ "revision": "2",
+ }
+
+ snapDeclFoo2, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDeclFoo2)
+ c.Assert(err, IsNil)
+ })()
+
+ // no error, kept the old one
+ err = assertstate.RefreshSnapDeclarations(s.state, 0)
+ c.Assert(err, IsNil)
+
+ a, err = assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(err, IsNil)
+ c.Check(a.(*asserts.SnapDeclaration).SnapName(), Equals, "fo-o")
+ c.Check(a.(*asserts.SnapDeclaration).Revision(), Equals, 1)
+}
+
+func (s *assertMgrSuite) TestValidateRefreshesNothing(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ validated, err := assertstate.ValidateRefreshes(s.state, nil, 0)
+ c.Assert(err, IsNil)
+ c.Check(validated, HasLen, 0)
+}
+
+func (s *assertMgrSuite) TestValidateRefreshesNoControl(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ snapDeclBar := s.snapDecl(c, "bar", nil)
+ s.stateFromDecl(snapDeclFoo, snap.R(7))
+ s.stateFromDecl(snapDeclBar, snap.R(3))
+
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBar)
+ c.Assert(err, IsNil)
+
+ fooRefresh := &snap.Info{
+ SideInfo: snap.SideInfo{RealName: "foo", SnapID: "foo-id", Revision: snap.R(9)},
+ }
+
+ validated, err := assertstate.ValidateRefreshes(s.state, []*snap.Info{fooRefresh}, 0)
+ c.Assert(err, IsNil)
+ c.Check(validated, DeepEquals, []*snap.Info{fooRefresh})
+}
+
+func (s *assertMgrSuite) TestValidateRefreshesMissingValidation(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ snapDeclBar := s.snapDecl(c, "bar", map[string]interface{}{
+ "refresh-control": []interface{}{"foo-id"},
+ })
+ s.stateFromDecl(snapDeclFoo, snap.R(7))
+ s.stateFromDecl(snapDeclBar, snap.R(3))
+
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBar)
+ c.Assert(err, IsNil)
+
+ fooRefresh := &snap.Info{
+ SideInfo: snap.SideInfo{RealName: "foo", SnapID: "foo-id", Revision: snap.R(9)},
+ }
+
+ validated, err := assertstate.ValidateRefreshes(s.state, []*snap.Info{fooRefresh}, 0)
+ c.Assert(err, ErrorMatches, `cannot refresh "foo" to revision 9: no validation by "bar"`)
+ c.Check(validated, HasLen, 0)
+}
+
+func (s *assertMgrSuite) TestValidateRefreshesValidationOK(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ snapDeclBar := s.snapDecl(c, "bar", map[string]interface{}{
+ "refresh-control": []interface{}{"foo-id"},
+ })
+ snapDeclBaz := s.snapDecl(c, "baz", map[string]interface{}{
+ "refresh-control": []interface{}{"foo-id"},
+ })
+ s.stateFromDecl(snapDeclFoo, snap.R(7))
+ s.stateFromDecl(snapDeclBar, snap.R(3))
+ s.stateFromDecl(snapDeclBaz, snap.R(1))
+ snapstate.Set(s.state, "local", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{
+ {RealName: "local", Revision: snap.R(-1)},
+ },
+ Current: snap.R(-1),
+ })
+
+ // validation by bar
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "bar-id",
+ "approved-snap-id": "foo-id",
+ "approved-snap-revision": "9",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ barValidation, err := s.dev1Signing.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(barValidation)
+ c.Assert(err, IsNil)
+
+ // validation by baz
+ headers = map[string]interface{}{
+ "series": "16",
+ "snap-id": "baz-id",
+ "approved-snap-id": "foo-id",
+ "approved-snap-revision": "9",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ bazValidation, err := s.dev1Signing.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(bazValidation)
+ c.Assert(err, IsNil)
+
+ err = assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBar)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBaz)
+ c.Assert(err, IsNil)
+
+ fooRefresh := &snap.Info{
+ SideInfo: snap.SideInfo{RealName: "foo", SnapID: "foo-id", Revision: snap.R(9)},
+ }
+
+ validated, err := assertstate.ValidateRefreshes(s.state, []*snap.Info{fooRefresh}, 0)
+ c.Assert(err, IsNil)
+ c.Check(validated, DeepEquals, []*snap.Info{fooRefresh})
+}
+
+func (s *assertMgrSuite) TestValidateRefreshesRevokedValidation(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ snapDeclBar := s.snapDecl(c, "bar", map[string]interface{}{
+ "refresh-control": []interface{}{"foo-id"},
+ })
+ snapDeclBaz := s.snapDecl(c, "baz", map[string]interface{}{
+ "refresh-control": []interface{}{"foo-id"},
+ })
+ s.stateFromDecl(snapDeclFoo, snap.R(7))
+ s.stateFromDecl(snapDeclBar, snap.R(3))
+ s.stateFromDecl(snapDeclBaz, snap.R(1))
+ snapstate.Set(s.state, "local", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{
+ {RealName: "local", Revision: snap.R(-1)},
+ },
+ Current: snap.R(-1),
+ })
+
+ // validation by bar
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "bar-id",
+ "approved-snap-id": "foo-id",
+ "approved-snap-revision": "9",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ barValidation, err := s.dev1Signing.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(barValidation)
+ c.Assert(err, IsNil)
+
+ // revoked validation by baz
+ headers = map[string]interface{}{
+ "series": "16",
+ "snap-id": "baz-id",
+ "approved-snap-id": "foo-id",
+ "approved-snap-revision": "9",
+ "revoked": "true",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ bazValidation, err := s.dev1Signing.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(bazValidation)
+ c.Assert(err, IsNil)
+
+ err = assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBar)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBaz)
+ c.Assert(err, IsNil)
+
+ fooRefresh := &snap.Info{
+ SideInfo: snap.SideInfo{RealName: "foo", SnapID: "foo-id", Revision: snap.R(9)},
+ }
+
+ validated, err := assertstate.ValidateRefreshes(s.state, []*snap.Info{fooRefresh}, 0)
+ c.Assert(err, ErrorMatches, `(?s).*cannot refresh "foo" to revision 9: validation by "baz" \(id "baz-id"\) revoked.*`)
+ c.Check(validated, HasLen, 0)
+}
+
+func (s *assertMgrSuite) TestBaseSnapDeclaration(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ r1 := assertstest.MockBuiltinBaseDeclaration(nil)
+ defer r1()
+
+ baseDecl, err := assertstate.BaseDeclaration(s.state)
+ c.Assert(err, Equals, asserts.ErrNotFound)
+ c.Check(baseDecl, IsNil)
+
+ r2 := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+plugs:
+ iface: true
+`))
+ defer r2()
+
+ baseDecl, err = assertstate.BaseDeclaration(s.state)
+ c.Assert(err, IsNil)
+ c.Check(baseDecl, NotNil)
+ c.Check(baseDecl.PlugRule("iface"), NotNil)
+}
+
+func (s *assertMgrSuite) TestSnapDeclaration(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // have a declaration in the system db
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+
+ _, err = assertstate.SnapDeclaration(s.state, "snap-id-other")
+ c.Check(err, Equals, asserts.ErrNotFound)
+
+ snapDecl, err := assertstate.SnapDeclaration(s.state, "foo-id")
+ c.Assert(err, IsNil)
+ c.Check(snapDecl.SnapName(), Equals, "foo")
+}
+
+func (s *assertMgrSuite) TestAutoAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // prereqs for developer assertions in the system db
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ // not from the store
+ aliases, err := assertstate.AutoAliases(s.state, &snap.Info{SuggestedName: "local"})
+ c.Assert(err, IsNil)
+ c.Check(aliases, HasLen, 0)
+
+ // missing
+ _, err = assertstate.AutoAliases(s.state, &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "baz",
+ SnapID: "baz-id",
+ },
+ })
+ c.Check(err, ErrorMatches, `internal error: cannot find snap-declaration for installed snap "baz": assertion not found`)
+
+ // empty list
+ // have a declaration in the system db
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ aliases, err = assertstate.AutoAliases(s.state, &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "foo",
+ SnapID: "foo-id",
+ },
+ })
+ c.Assert(err, IsNil)
+ c.Check(aliases, HasLen, 0)
+
+ // some aliases
+ snapDeclFoo = s.snapDecl(c, "foo", map[string]interface{}{
+ "auto-aliases": []interface{}{"alias1", "alias2"},
+ "revision": "1",
+ })
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ aliases, err = assertstate.AutoAliases(s.state, &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "foo",
+ SnapID: "foo-id",
+ },
+ })
+ c.Assert(err, IsNil)
+ c.Check(aliases, DeepEquals, []string{"alias1", "alias2"})
+}
+
+func (s *assertMgrSuite) TestPublisher(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // have a declaration in the system db
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+
+ _, err = assertstate.SnapDeclaration(s.state, "snap-id-other")
+ c.Check(err, Equals, asserts.ErrNotFound)
+
+ acct, err := assertstate.Publisher(s.state, "foo-id")
+ c.Assert(err, IsNil)
+ c.Check(acct.AccountID(), Equals, s.dev1Acct.AccountID())
+ c.Check(acct.Username(), Equals, "developer1")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package assertstate
+
+// expose for testing
+var (
+ DoFetch = doFetch
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package auth
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+
+ "gopkg.in/macaroon.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// AuthState represents current authenticated users as tracked in state
+type AuthState struct {
+ LastID int `json:"last-id"`
+ Users []UserState `json:"users"`
+ Device *DeviceState `json:"device,omitempty"`
+ MacaroonKey []byte `json:"macaroon-key,omitempty"`
+}
+
+// DeviceState represents the device's identity and store credentials
+type DeviceState struct {
+ Brand string `json:"brand,omitempty"`
+ Model string `json:"model,omitempty"`
+ Serial string `json:"serial,omitempty"`
+
+ KeyID string `json:"key-id,omitempty"`
+
+ SessionMacaroon string `json:"session-macaroon,omitempty"`
+}
+
+// UserState represents an authenticated user
+type UserState struct {
+ ID int `json:"id"`
+ Username string `json:"username,omitempty"`
+ Email string `json:"email,omitempty"`
+ Macaroon string `json:"macaroon,omitempty"`
+ Discharges []string `json:"discharges,omitempty"`
+ StoreMacaroon string `json:"store-macaroon,omitempty"`
+ StoreDischarges []string `json:"store-discharges,omitempty"`
+}
+
+// MacaroonSerialize returns a store-compatible serialized representation of the given macaroon
+func MacaroonSerialize(m *macaroon.Macaroon) (string, error) {
+ marshalled, err := m.MarshalBinary()
+ if err != nil {
+ return "", err
+ }
+ encoded := base64.RawURLEncoding.EncodeToString(marshalled)
+ return encoded, nil
+}
+
+// MacaroonDeserialize returns a deserialized macaroon from a given store-compatible serialization
+func MacaroonDeserialize(serializedMacaroon string) (*macaroon.Macaroon, error) {
+ var m macaroon.Macaroon
+ decoded, err := base64.RawURLEncoding.DecodeString(serializedMacaroon)
+ if err != nil {
+ return nil, err
+ }
+ err = m.UnmarshalBinary(decoded)
+ if err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// generateMacaroonKey generates a random key to sign snapd macaroons
+func generateMacaroonKey() ([]byte, error) {
+ key := make([]byte, 32)
+ if _, err := rand.Read(key); err != nil {
+ return nil, err
+ }
+ return key, nil
+}
+
+const snapdMacaroonLocation = "snapd"
+
+// newUserMacaroon returns a snapd macaroon for the given username
+func newUserMacaroon(macaroonKey []byte, userID int) (string, error) {
+ userMacaroon, err := macaroon.New(macaroonKey, strconv.Itoa(userID), snapdMacaroonLocation)
+ if err != nil {
+ return "", fmt.Errorf("cannot create macaroon for snapd user: %s", err)
+ }
+
+ serializedMacaroon, err := MacaroonSerialize(userMacaroon)
+ if err != nil {
+ return "", fmt.Errorf("cannot serialize macaroon for snapd user: %s", err)
+ }
+
+ return serializedMacaroon, nil
+}
+
+// NewUser tracks a new authenticated user and saves its details in the state
+func NewUser(st *state.State, username, email, macaroon string, discharges []string) (*UserState, error) {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err == state.ErrNoState {
+ authStateData = AuthState{}
+ } else if err != nil {
+ return nil, err
+ }
+
+ if authStateData.MacaroonKey == nil {
+ authStateData.MacaroonKey, err = generateMacaroonKey()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ authStateData.LastID++
+
+ localMacaroon, err := newUserMacaroon(authStateData.MacaroonKey, authStateData.LastID)
+ if err != nil {
+ return nil, err
+ }
+
+ sort.Strings(discharges)
+ authenticatedUser := UserState{
+ ID: authStateData.LastID,
+ Username: username,
+ Email: email,
+ Macaroon: localMacaroon,
+ Discharges: nil,
+ StoreMacaroon: macaroon,
+ StoreDischarges: discharges,
+ }
+ authStateData.Users = append(authStateData.Users, authenticatedUser)
+
+ st.Set("auth", authStateData)
+
+ return &authenticatedUser, nil
+}
+
+// RemoveUser removes a user from the state given its ID
+func RemoveUser(st *state.State, userID int) error {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err != nil {
+ return err
+ }
+
+ for i := range authStateData.Users {
+ if authStateData.Users[i].ID == userID {
+ // delete without preserving order
+ n := len(authStateData.Users) - 1
+ authStateData.Users[i] = authStateData.Users[n]
+ authStateData.Users[n] = UserState{}
+ authStateData.Users = authStateData.Users[:n]
+ st.Set("auth", authStateData)
+ return nil
+ }
+ }
+
+ return fmt.Errorf("invalid user")
+}
+
+func Users(st *state.State) ([]*UserState, error) {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err == state.ErrNoState {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ users := make([]*UserState, len(authStateData.Users))
+ for i := range authStateData.Users {
+ users[i] = &authStateData.Users[i]
+ }
+ return users, nil
+}
+
+// User returns a user from the state given its ID
+func User(st *state.State, id int) (*UserState, error) {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, user := range authStateData.Users {
+ if user.ID == id {
+ return &user, nil
+ }
+ }
+ return nil, fmt.Errorf("invalid user")
+}
+
+// UpdateUser updates user in state
+func UpdateUser(st *state.State, user *UserState) error {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err != nil {
+ return err
+ }
+
+ for i := range authStateData.Users {
+ if authStateData.Users[i].ID == user.ID {
+ authStateData.Users[i] = *user
+ st.Set("auth", authStateData)
+ return nil
+ }
+ }
+
+ return fmt.Errorf("invalid user")
+}
+
+// Device returns the device details from the state.
+func Device(st *state.State) (*DeviceState, error) {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err == state.ErrNoState {
+ return &DeviceState{}, nil
+ } else if err != nil {
+ return nil, err
+ }
+
+ if authStateData.Device == nil {
+ return &DeviceState{}, nil
+ }
+
+ return authStateData.Device, nil
+}
+
+// SetDevice updates the device details in the state.
+func SetDevice(st *state.State, device *DeviceState) error {
+ var authStateData AuthState
+
+ err := st.Get("auth", &authStateData)
+ if err == state.ErrNoState {
+ authStateData = AuthState{}
+ } else if err != nil {
+ return err
+ }
+
+ authStateData.Device = device
+ st.Set("auth", authStateData)
+
+ return nil
+}
+
+var ErrInvalidAuth = fmt.Errorf("invalid authentication")
+
+// CheckMacaroon returns the UserState for the given macaroon/discharges credentials
+func CheckMacaroon(st *state.State, macaroon string, discharges []string) (*UserState, error) {
+ var authStateData AuthState
+ err := st.Get("auth", &authStateData)
+ if err != nil {
+ return nil, ErrInvalidAuth
+ }
+
+ snapdMacaroon, err := MacaroonDeserialize(macaroon)
+ if err != nil {
+ return nil, ErrInvalidAuth
+ }
+ // attempt snapd macaroon verification
+ if snapdMacaroon.Location() == snapdMacaroonLocation {
+ // no caveats to check so far
+ check := func(caveat string) error { return nil }
+ // ignoring discharges, unused for snapd macaroons atm
+ err = snapdMacaroon.Verify(authStateData.MacaroonKey, check, nil)
+ if err != nil {
+ return nil, ErrInvalidAuth
+ }
+ macaroonID := snapdMacaroon.Id()
+ userID, err := strconv.Atoi(macaroonID)
+ if err != nil {
+ return nil, ErrInvalidAuth
+ }
+ user, err := User(st, userID)
+ if err != nil {
+ return nil, ErrInvalidAuth
+ }
+ if macaroon != user.Macaroon {
+ return nil, ErrInvalidAuth
+ }
+ return user, nil
+ }
+
+ // if macaroon is not a snapd macaroon, fallback to previous token-style check
+NextUser:
+ for _, user := range authStateData.Users {
+ if user.Macaroon != macaroon {
+ continue
+ }
+ if len(user.Discharges) != len(discharges) {
+ continue
+ }
+ // sort discharges (stored users' discharges are already sorted)
+ sort.Strings(discharges)
+ for i, d := range user.Discharges {
+ if d != discharges[i] {
+ continue NextUser
+ }
+ }
+ return &user, nil
+ }
+ return nil, ErrInvalidAuth
+}
+
+// DeviceAssertions helps exposing the assertions about device identity.
+// All methods should return state.ErrNoState if the underlying needed
+// information is not (yet) available.
+type DeviceAssertions interface {
+ // Model returns the device model assertion.
+ Model() (*asserts.Model, error)
+ // Serial returns the device model assertion.
+ Serial() (*asserts.Serial, error)
+
+ // DeviceSessionRequest produces a device-session-request with the given nonce, it also returns the device serial assertion.
+ DeviceSessionRequest(nonce string) (*asserts.DeviceSessionRequest, *asserts.Serial, error)
+}
+
+var (
+ // ErrNoSerial indicates that a device serial is not set yet.
+ ErrNoSerial = errors.New("no device serial yet")
+)
+
+// An AuthContext exposes authorization data and handles its updates.
+type AuthContext interface {
+ Device() (*DeviceState, error)
+
+ UpdateDeviceAuth(device *DeviceState, sessionMacaroon string) (actual *DeviceState, err error)
+
+ UpdateUserAuth(user *UserState, discharges []string) (actual *UserState, err error)
+
+ StoreID(fallback string) (string, error)
+
+ DeviceSessionRequest(nonce string) (devSessionRequest []byte, serial []byte, err error)
+}
+
+// authContext helps keeping track of auth data in the state and exposing it.
+type authContext struct {
+ state *state.State
+ deviceAsserts DeviceAssertions
+}
+
+// NewAuthContext returns an AuthContext for state.
+func NewAuthContext(st *state.State, deviceAsserts DeviceAssertions) AuthContext {
+ return &authContext{state: st, deviceAsserts: deviceAsserts}
+}
+
+// Device returns current device state.
+func (ac *authContext) Device() (*DeviceState, error) {
+ ac.state.Lock()
+ defer ac.state.Unlock()
+
+ return Device(ac.state)
+}
+
+// UpdateDeviceAuth updates the device auth details in state.
+// The last update wins but other device details are left unchanged.
+// It returns the updated device state value.
+func (ac *authContext) UpdateDeviceAuth(device *DeviceState, newSessionMacaroon string) (actual *DeviceState, err error) {
+ ac.state.Lock()
+ defer ac.state.Unlock()
+
+ cur, err := Device(ac.state)
+ if err != nil {
+ return nil, err
+ }
+
+ // just do it, last update wins
+ cur.SessionMacaroon = newSessionMacaroon
+ if err := SetDevice(ac.state, cur); err != nil {
+ return nil, fmt.Errorf("internal error: cannot update just read device state: %v", err)
+ }
+
+ return cur, nil
+}
+
+// UpdateUserAuth updates the user auth details in state.
+// The last update wins but other user details are left unchanged.
+// It returns the updated user state value.
+func (ac *authContext) UpdateUserAuth(user *UserState, newDischarges []string) (actual *UserState, err error) {
+ ac.state.Lock()
+ defer ac.state.Unlock()
+
+ cur, err := User(ac.state, user.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ // just do it, last update wins
+ cur.StoreDischarges = newDischarges
+ if err := UpdateUser(ac.state, cur); err != nil {
+ return nil, fmt.Errorf("internal error: cannot update just read user state: %v", err)
+ }
+
+ return cur, nil
+}
+
+// StoreID returns the store id according to system state or
+// the fallback one if the state has none set (yet).
+func (ac *authContext) StoreID(fallback string) (string, error) {
+ storeID := os.Getenv("UBUNTU_STORE_ID")
+ if storeID != "" {
+ return storeID, nil
+ }
+ if ac.deviceAsserts != nil {
+ mod, err := ac.deviceAsserts.Model()
+ if err != nil && err != state.ErrNoState {
+ return "", err
+ }
+ if err == nil {
+ storeID = mod.Store()
+ }
+ }
+ if storeID != "" {
+ return storeID, nil
+ }
+ return fallback, nil
+}
+
+// DeviceSessionRequest produces a device-session-request with the given nonce, it also returns the encoded device serial assertion. It returns ErrNoSerial if the device serial is not yet initialized.
+func (ac *authContext) DeviceSessionRequest(nonce string) (deviceSessionRequest []byte, serial []byte, err error) {
+ if ac.deviceAsserts == nil {
+ return nil, nil, ErrNoSerial
+ }
+ req, ser, err := ac.deviceAsserts.DeviceSessionRequest(nonce)
+ if err == state.ErrNoState {
+ return nil, nil, ErrNoSerial
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ return asserts.Encode(req), asserts.Encode(ser), nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package auth_test
+
+import (
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/macaroon.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// Hook up gocheck into the "go test" runner.
+func Test(t *testing.T) { TestingT(t) }
+
+type authSuite struct {
+ state *state.State
+}
+
+var _ = Suite(&authSuite{})
+
+func (as *authSuite) SetUpTest(c *C) {
+ as.state = state.New(nil)
+}
+
+func (s *authSuite) TestMacaroonSerialize(c *C) {
+ m, err := macaroon.New([]byte("secret"), "some-id", "location")
+ c.Check(err, IsNil)
+
+ serialized, err := auth.MacaroonSerialize(m)
+ c.Check(err, IsNil)
+
+ deserialized, err := auth.MacaroonDeserialize(serialized)
+ c.Check(err, IsNil)
+ c.Check(deserialized, DeepEquals, m)
+}
+
+func (s *authSuite) TestMacaroonSerializeDeserializeStoreMacaroon(c *C) {
+ // sample serialized macaroon using store server setup.
+ serialized := `MDAxNmxvY2F0aW9uIGxvY2F0aW9uCjAwMTdpZGVudGlmaWVyIHNvbWUgaWQKMDAwZmNpZCBjYXZlYXQKMDAxOWNpZCAzcmQgcGFydHkgY2F2ZWF0CjAwNTF2aWQgcyvpXSVlMnj9wYw5b-WPCLjTnO_8lVzBrRr8tJfu9tOhPORbsEOFyBwPOM_YiiXJ_qh-Pp8HY0HsUueCUY4dxONLIxPWTdMzCjAwMTJjbCByZW1vdGUuY29tCjAwMmZzaWduYXR1cmUgcm_Gdz75wUCWF9KGXZQEANhwfvBcLNt9xXGfAmxurPMK`
+
+ deserialized, err := auth.MacaroonDeserialize(serialized)
+ c.Check(err, IsNil)
+
+ // expected json serialization of the above macaroon
+ jsonData := []byte(`{"caveats":[{"cid":"caveat"},{"cid":"3rd party caveat","vid":"cyvpXSVlMnj9wYw5b-WPCLjTnO_8lVzBrRr8tJfu9tOhPORbsEOFyBwPOM_YiiXJ_qh-Pp8HY0HsUueCUY4dxONLIxPWTdMz","cl":"remote.com"}],"location":"location","identifier":"some id","signature":"726fc6773ef9c1409617d2865d940400d8707ef05c2cdb7dc5719f026c6eacf3"}`)
+
+ var expected macaroon.Macaroon
+ err = expected.UnmarshalJSON(jsonData)
+ c.Check(err, IsNil)
+ c.Check(deserialized, DeepEquals, &expected)
+
+ // reserializing the macaroon should give us the same original store serialization
+ reserialized, err := auth.MacaroonSerialize(deserialized)
+ c.Check(err, IsNil)
+ c.Check(reserialized, Equals, serialized)
+}
+
+func (s *authSuite) TestMacaroonDeserializeInvalidData(c *C) {
+ serialized := "invalid-macaroon-data"
+
+ deserialized, err := auth.MacaroonDeserialize(serialized)
+ c.Check(deserialized, IsNil)
+ c.Check(err, NotNil)
+}
+
+func (as *authSuite) TestNewUser(c *C) {
+ as.state.Lock()
+ user, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ // check snapd macaroon was generated for the local user
+ var authStateData auth.AuthState
+ as.state.Lock()
+ err = as.state.Get("auth", &authStateData)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(authStateData.MacaroonKey, NotNil)
+ expectedMacaroon, err := macaroon.New(authStateData.MacaroonKey, "1", "snapd")
+ c.Check(err, IsNil)
+ expectedSerializedMacaroon, err := auth.MacaroonSerialize(expectedMacaroon)
+ c.Check(err, IsNil)
+
+ expected := &auth.UserState{
+ ID: 1,
+ Username: "username",
+ Email: "email@test.com",
+ Macaroon: expectedSerializedMacaroon,
+ Discharges: nil,
+ StoreMacaroon: "macaroon",
+ StoreDischarges: []string{"discharge"},
+ }
+ c.Check(user, DeepEquals, expected)
+}
+
+func (as *authSuite) TestNewUserSortsDischarges(c *C) {
+ as.state.Lock()
+ user, err := auth.NewUser(as.state, "", "email@test.com", "macaroon", []string{"discharge2", "discharge1"})
+ c.Assert(err, IsNil)
+ as.state.Unlock()
+
+ expected := []string{"discharge1", "discharge2"}
+ c.Check(user.StoreDischarges, DeepEquals, expected)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, 1)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState.StoreDischarges, DeepEquals, expected)
+}
+
+func (as *authSuite) TestNewUserAddsToExistent(c *C) {
+ as.state.Lock()
+ firstUser, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ // adding a new one
+ as.state.Lock()
+ user, err := auth.NewUser(as.state, "new_username", "new_email@test.com", "new_macaroon", []string{"new_discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(user.ID, Equals, 2)
+ c.Check(user.Username, Equals, "new_username")
+ c.Check(user.Email, Equals, "new_email@test.com")
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, 2)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState.ID, Equals, 2)
+ c.Check(userFromState.Username, Equals, "new_username")
+ c.Check(userFromState.Email, Equals, "new_email@test.com")
+
+ // first user is still in the state
+ as.state.Lock()
+ userFromState, err = auth.User(as.state, 1)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState, DeepEquals, firstUser)
+}
+
+func (as *authSuite) TestCheckMacaroonNoAuthData(c *C) {
+ as.state.Lock()
+ user, err := auth.CheckMacaroon(as.state, "macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ c.Check(err, Equals, auth.ErrInvalidAuth)
+ c.Check(user, IsNil)
+}
+
+func (as *authSuite) TestCheckMacaroonInvalidAuth(c *C) {
+ as.state.Lock()
+ user, err := auth.CheckMacaroon(as.state, "other-macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ c.Check(err, Equals, auth.ErrInvalidAuth)
+ c.Check(user, IsNil)
+
+ as.state.Lock()
+ _, err = auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ user, err = auth.CheckMacaroon(as.state, "other-macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ c.Check(err, Equals, auth.ErrInvalidAuth)
+ c.Check(user, IsNil)
+}
+
+func (as *authSuite) TestCheckMacaroonValidUser(c *C) {
+ as.state.Lock()
+ expectedUser, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ user, err := auth.CheckMacaroon(as.state, expectedUser.Macaroon, expectedUser.Discharges)
+ as.state.Unlock()
+
+ c.Check(err, IsNil)
+ c.Check(user, DeepEquals, expectedUser)
+}
+
+func (as *authSuite) TestCheckMacaroonValidUserOldStyle(c *C) {
+ // create a fake store-deserializable macaroon
+ m, err := macaroon.New([]byte("secret"), "some-id", "location")
+ c.Check(err, IsNil)
+ serializedMacaroon, err := auth.MacaroonSerialize(m)
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ expectedUser, err := auth.NewUser(as.state, "username", "email@test.com", serializedMacaroon, []string{"discharge"})
+ c.Check(err, IsNil)
+ // set user local macaroons with store macaroons
+ expectedUser.Macaroon = expectedUser.StoreMacaroon
+ expectedUser.Discharges = expectedUser.StoreDischarges
+ err = auth.UpdateUser(as.state, expectedUser)
+ c.Check(err, IsNil)
+ as.state.Unlock()
+
+ as.state.Lock()
+ user, err := auth.CheckMacaroon(as.state, expectedUser.Macaroon, expectedUser.Discharges)
+ as.state.Unlock()
+
+ c.Check(err, IsNil)
+ c.Check(user, DeepEquals, expectedUser)
+}
+
+func (as *authSuite) TestCheckMacaroonInvalidAuthMalformedMacaroon(c *C) {
+ var authStateData auth.AuthState
+ as.state.Lock()
+ // create a new user to ensure there is a MacaroonKey setup
+ _, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ c.Check(err, IsNil)
+ // get AuthState to get signing MacaroonKey
+ err = as.state.Get("auth", &authStateData)
+ c.Check(err, IsNil)
+ as.state.Unlock()
+
+ // setup a macaroon for an invalid user
+ invalidMacaroon, err := macaroon.New(authStateData.MacaroonKey, "invalid", "snapd")
+ c.Check(err, IsNil)
+ serializedInvalidMacaroon, err := auth.MacaroonSerialize(invalidMacaroon)
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ user, err := auth.CheckMacaroon(as.state, serializedInvalidMacaroon, nil)
+ as.state.Unlock()
+
+ c.Check(err, Equals, auth.ErrInvalidAuth)
+ c.Check(user, IsNil)
+}
+
+func (as *authSuite) TestUserForNoAuthInState(c *C) {
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, 42)
+ as.state.Unlock()
+ c.Check(err, NotNil)
+ c.Check(userFromState, IsNil)
+}
+
+func (as *authSuite) TestUserForNonExistent(c *C) {
+ as.state.Lock()
+ _, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, 42)
+ c.Check(err, ErrorMatches, "invalid user")
+ c.Check(userFromState, IsNil)
+}
+
+func (as *authSuite) TestUser(c *C) {
+ as.state.Lock()
+ user, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, 1)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState, DeepEquals, user)
+}
+
+func (as *authSuite) TestUpdateUser(c *C) {
+ as.state.Lock()
+ user, _ := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ user.Username = "different"
+ user.StoreDischarges = []string{"updated-discharge"}
+
+ as.state.Lock()
+ err := auth.UpdateUser(as.state, user)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, user.ID)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState, DeepEquals, user)
+}
+
+func (as *authSuite) TestUpdateUserInvalid(c *C) {
+ as.state.Lock()
+ _, _ = auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ user := &auth.UserState{
+ ID: 102,
+ Username: "username",
+ Macaroon: "macaroon",
+ }
+
+ as.state.Lock()
+ err := auth.UpdateUser(as.state, user)
+ as.state.Unlock()
+ c.Assert(err, ErrorMatches, "invalid user")
+}
+
+func (as *authSuite) TestRemove(c *C) {
+ as.state.Lock()
+ user, err := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ _, err = auth.User(as.state, user.ID)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ err = auth.RemoveUser(as.state, user.ID)
+ as.state.Unlock()
+ c.Assert(err, IsNil)
+
+ as.state.Lock()
+ _, err = auth.User(as.state, user.ID)
+ as.state.Unlock()
+ c.Check(err, ErrorMatches, "invalid user")
+
+ as.state.Lock()
+ err = auth.RemoveUser(as.state, user.ID)
+ as.state.Unlock()
+ c.Assert(err, ErrorMatches, "invalid user")
+}
+
+func (as *authSuite) TestSetDevice(c *C) {
+ as.state.Lock()
+ device, err := auth.Device(as.state)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(device, DeepEquals, &auth.DeviceState{})
+
+ as.state.Lock()
+ err = auth.SetDevice(as.state, &auth.DeviceState{Brand: "some-brand"})
+ c.Check(err, IsNil)
+ device, err = auth.Device(as.state)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(device, DeepEquals, &auth.DeviceState{Brand: "some-brand"})
+}
+
+func (as *authSuite) TestAuthContextUpdateUserAuth(c *C) {
+ as.state.Lock()
+ user, _ := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ newDischarges := []string{"updated-discharge"}
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ user, err := authContext.UpdateUserAuth(user, newDischarges)
+ c.Check(err, IsNil)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, user.ID)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState, DeepEquals, user)
+ c.Check(userFromState.Discharges, IsNil)
+ c.Check(user.StoreDischarges, DeepEquals, newDischarges)
+}
+
+func (as *authSuite) TestAuthContextUpdateUserAuthOtherUpdate(c *C) {
+ as.state.Lock()
+ user, _ := auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ otherUpdateUser := *user
+ otherUpdateUser.Macaroon = "macaroon2"
+ otherUpdateUser.StoreDischarges = []string{"other-discharges"}
+ err := auth.UpdateUser(as.state, &otherUpdateUser)
+ as.state.Unlock()
+ c.Assert(err, IsNil)
+
+ newDischarges := []string{"updated-discharge"}
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ // last discharges win
+ curUser, err := authContext.UpdateUserAuth(user, newDischarges)
+ c.Assert(err, IsNil)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, user.ID)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState, DeepEquals, curUser)
+ c.Check(curUser, DeepEquals, &auth.UserState{
+ ID: user.ID,
+ Username: "username",
+ Email: "email@test.com",
+ Macaroon: "macaroon2",
+ Discharges: nil,
+ StoreMacaroon: "macaroon",
+ StoreDischarges: newDischarges,
+ })
+}
+
+func (as *authSuite) TestAuthContextUpdateUserAuthInvalid(c *C) {
+ as.state.Lock()
+ _, _ = auth.NewUser(as.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+
+ user := &auth.UserState{
+ ID: 102,
+ Username: "username",
+ Macaroon: "macaroon",
+ }
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ _, err := authContext.UpdateUserAuth(user, nil)
+ c.Assert(err, ErrorMatches, "invalid user")
+}
+
+func (as *authSuite) TestAuthContextDeviceForNonExistent(c *C) {
+ authContext := auth.NewAuthContext(as.state, nil)
+
+ device, err := authContext.Device()
+ c.Check(err, IsNil)
+ c.Check(device, DeepEquals, &auth.DeviceState{})
+}
+
+func (as *authSuite) TestAuthContextDevice(c *C) {
+ device := &auth.DeviceState{Brand: "some-brand"}
+ as.state.Lock()
+ err := auth.SetDevice(as.state, device)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ authContext := auth.NewAuthContext(as.state, nil)
+
+ deviceFromState, err := authContext.Device()
+ c.Check(err, IsNil)
+ c.Check(deviceFromState, DeepEquals, device)
+}
+
+func (as *authSuite) TestAuthContextUpdateDeviceAuth(c *C) {
+ as.state.Lock()
+ device, err := auth.Device(as.state)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(device, DeepEquals, &auth.DeviceState{})
+
+ sessionMacaroon := "the-device-macaroon"
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ device, err = authContext.UpdateDeviceAuth(device, sessionMacaroon)
+ c.Check(err, IsNil)
+
+ deviceFromState, err := authContext.Device()
+ c.Check(err, IsNil)
+ c.Check(deviceFromState, DeepEquals, device)
+ c.Check(deviceFromState.SessionMacaroon, DeepEquals, sessionMacaroon)
+}
+
+func (as *authSuite) TestAuthContextUpdateDeviceAuthOtherUpdate(c *C) {
+ as.state.Lock()
+ device, _ := auth.Device(as.state)
+ otherUpdateDevice := *device
+ otherUpdateDevice.SessionMacaroon = "othe-session-macaroon"
+ otherUpdateDevice.KeyID = "KEYID"
+ err := auth.SetDevice(as.state, &otherUpdateDevice)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ sessionMacaroon := "the-device-macaroon"
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ curDevice, err := authContext.UpdateDeviceAuth(device, sessionMacaroon)
+ c.Assert(err, IsNil)
+
+ as.state.Lock()
+ deviceFromState, err := auth.Device(as.state)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(deviceFromState, DeepEquals, curDevice)
+ c.Check(curDevice, DeepEquals, &auth.DeviceState{
+ KeyID: "KEYID",
+ SessionMacaroon: sessionMacaroon,
+ })
+}
+
+func (as *authSuite) TestAuthContextStoreIDFallback(c *C) {
+ authContext := auth.NewAuthContext(as.state, nil)
+
+ storeID, err := authContext.StoreID("store-id")
+ c.Assert(err, IsNil)
+ c.Check(storeID, Equals, "store-id")
+}
+
+func (as *authSuite) TestAuthContextStoreIDFromEnv(c *C) {
+ authContext := auth.NewAuthContext(as.state, nil)
+
+ os.Setenv("UBUNTU_STORE_ID", "env-store-id")
+ defer os.Unsetenv("UBUNTU_STORE_ID")
+ storeID, err := authContext.StoreID("")
+ c.Assert(err, IsNil)
+ c.Check(storeID, Equals, "env-store-id")
+}
+func (as *authSuite) TestAuthContextDeviceSessionRequestNilDeviceAssertions(c *C) {
+ authContext := auth.NewAuthContext(as.state, nil)
+
+ _, _, err := authContext.DeviceSessionRequest("NONCE")
+ c.Check(err, Equals, auth.ErrNoSerial)
+}
+
+const (
+ exModel = `type: model
+authority-id: my-brand
+series: 16
+brand-id: my-brand
+model: baz-3000
+architecture: armhf
+gadget: gadget
+kernel: kernel
+store: my-brand-store-id
+timestamp: 2016-08-20T13:00:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=`
+
+ exSerial = `type: serial
+authority-id: my-brand
+brand-id: my-brand
+model: baz-3000
+serial: 9999
+device-key:
+ AcbBTQRWhcGAARAAtJGIguK7FhSyRxL/6jvdy0zAgGCjC1xVNFzeF76p5G8BXNEEHZUHK+z8Gr2J
+ inVrpvhJhllf5Ob2dIMH2YQbC9jE1kjbzvuauQGDqk6tNQm0i3KDeHCSPgVN+PFXPwKIiLrh66Po
+ AC7OfR1rFUgCqu0jch0H6Nue0ynvEPiY4dPeXq7mCdpDr5QIAM41L+3hg0OdzvO8HMIGZQpdF6jP
+ 7fkkVMROYvHUOJ8kknpKE7FiaNNpH7jK1qNxOYhLeiioX0LYrdmTvdTWHrSKZc82ZmlDjpKc4hUx
+ VtTXMAysw7CzIdREPom/vJklnKLvZt+Wk5AEF5V5YKnuT3pY+fjVMZ56GtTEeO/Er/oLk/n2xUK5
+ fD5DAyW/9z0ygzwTbY5IuWXyDfYneL4nXwWOEgg37Z4+8mTH+ftTz2dl1x1KIlIR2xo0kxf9t8K+
+ jlr13vwF1+QReMCSUycUsZ2Eep5XhjI+LG7G1bMSGqodZTIOXLkIy6+3iJ8Z/feIHlJ0ELBDyFbl
+ Yy04Sf9LI148vJMsYenonkoWejWdMi8iCUTeaZydHJEUBU/RbNFLjCWa6NIUe9bfZgLiOOZkps54
+ +/AL078ri/tGjo/5UGvezSmwrEoWJyqrJt2M69N2oVDLJcHeo2bUYPtFC2Kfb2je58JrJ+llifdg
+ rAsxbnHXiXyVimUAEQEAAQ==
+device-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+timestamp: 2016-08-24T21:55:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=`
+
+ exDeviceSessionRequest = `type: device-session-request
+brand-id: my-brand
+model: baz-3000
+serial: 9999
+nonce: @NONCE@
+timestamp: @TS@
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=`
+)
+
+type testDeviceAssertions struct {
+ nothing bool
+}
+
+func (da *testDeviceAssertions) Model() (*asserts.Model, error) {
+ if da.nothing {
+ return nil, state.ErrNoState
+ }
+ a, err := asserts.Decode([]byte(exModel))
+ if err != nil {
+ return nil, err
+ }
+ return a.(*asserts.Model), nil
+}
+
+func (da *testDeviceAssertions) Serial() (*asserts.Serial, error) {
+ if da.nothing {
+ return nil, state.ErrNoState
+ }
+ a, err := asserts.Decode([]byte(exSerial))
+ if err != nil {
+ return nil, err
+ }
+ return a.(*asserts.Serial), nil
+}
+
+func (da *testDeviceAssertions) DeviceSessionRequest(nonce string) (*asserts.DeviceSessionRequest, *asserts.Serial, error) {
+ if da.nothing {
+ return nil, nil, state.ErrNoState
+ }
+ ex := strings.Replace(exDeviceSessionRequest, "@NONCE@", nonce, 1)
+ ex = strings.Replace(ex, "@TS@", time.Now().Format(time.RFC3339), 1)
+ a1, err := asserts.Decode([]byte(ex))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ a2, err := asserts.Decode([]byte(exSerial))
+ if err != nil {
+ return nil, nil, err
+ }
+ return a1.(*asserts.DeviceSessionRequest), a2.(*asserts.Serial), nil
+}
+
+func (as *authSuite) TestAuthContextMissingDeviceAssertions(c *C) {
+ // no assertions in state
+ authContext := auth.NewAuthContext(as.state, &testDeviceAssertions{nothing: true})
+
+ _, _, err := authContext.DeviceSessionRequest("NONCE")
+ c.Check(err, Equals, auth.ErrNoSerial)
+
+ storeID, err := authContext.StoreID("fallback")
+ c.Assert(err, IsNil)
+ c.Check(storeID, Equals, "fallback")
+}
+
+func (as *authSuite) TestAuthContextWithDeviceAssertions(c *C) {
+ // having assertions in state
+ authContext := auth.NewAuthContext(as.state, &testDeviceAssertions{})
+
+ req, serial, err := authContext.DeviceSessionRequest("NONCE-1")
+ c.Assert(err, IsNil)
+ c.Check(strings.Contains(string(req), "nonce: NONCE-1\n"), Equals, true)
+ c.Check(strings.Contains(string(req), "serial: 9999\n"), Equals, true)
+ c.Check(strings.Contains(string(serial), "serial: 9999\n"), Equals, true)
+
+ storeID, err := authContext.StoreID("store-id")
+ c.Assert(err, IsNil)
+ c.Check(storeID, Equals, "my-brand-store-id")
+}
+
+func (as *authSuite) TestUsers(c *C) {
+ as.state.Lock()
+ user1, err1 := auth.NewUser(as.state, "user1", "email1@test.com", "macaroon", []string{"discharge"})
+ user2, err2 := auth.NewUser(as.state, "user2", "email2@test.com", "macaroon", []string{"discharge"})
+ as.state.Unlock()
+ c.Check(err1, IsNil)
+ c.Check(err2, IsNil)
+
+ as.state.Lock()
+ users, err := auth.Users(as.state)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(users, DeepEquals, []*auth.UserState{user1, user2})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package overlord
+
+import (
+ "time"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type overlordStateBackend struct {
+ path string
+ ensureBefore func(d time.Duration)
+ requestRestart func(t state.RestartType)
+}
+
+func (osb *overlordStateBackend) Checkpoint(data []byte) error {
+ return osutil.AtomicWriteFile(osb.path, data, 0600, 0)
+}
+
+func (osb *overlordStateBackend) EnsureBefore(d time.Duration) {
+ osb.ensureBefore(d)
+}
+
+func (osb *overlordStateBackend) RequestRestart(t state.RestartType) {
+ osb.requestRestart(t)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package configstate implements the manager and state aspects responsible for
+// the configuration of snaps.
+package configstate
+
+import (
+ "regexp"
+
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// ConfigManager is responsible for the maintenance of per-snap configuration in
+// the system state.
+type ConfigManager struct {
+ state *state.State
+}
+
+// Manager returns a new ConfigManager.
+func Manager(s *state.State, hookManager *hookstate.HookManager) (*ConfigManager, error) {
+ manager := &ConfigManager{
+ state: s,
+ }
+
+ hookManager.Register(regexp.MustCompile("^configure$"), newConfigureHandler)
+
+ return manager, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate
+
+var NewConfigureHandler = newConfigureHandler
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate
+
+import "github.com/snapcore/snapd/overlord/hookstate"
+
+// configureHandler is the handler for the configure hook.
+type configureHandler struct {
+ context *hookstate.Context
+}
+
+// cachedTransaction is the index into the context cache where the initialized
+// transaction is stored.
+type cachedTransaction struct{}
+
+// ContextTransaction retrieves the transaction cached within the context (and
+// creates one if it hasn't already been cached).
+func ContextTransaction(context *hookstate.Context) *Transaction {
+ // Check for one already cached
+ transaction, ok := context.Cached(cachedTransaction{}).(*Transaction)
+ if ok {
+ return transaction
+ }
+
+ // It wasn't already cached, so create and cache a new one
+ transaction = NewTransaction(context.State())
+
+ context.OnDone(func() error {
+ transaction.Commit()
+ return nil
+ })
+
+ context.Cache(cachedTransaction{}, transaction)
+ return transaction
+}
+
+func newConfigureHandler(context *hookstate.Context) hookstate.Handler {
+ return &configureHandler{context: context}
+}
+
+// Before is called by the HookManager before the configure hook is run.
+func (h *configureHandler) Before() error {
+ h.context.Lock()
+ defer h.context.Unlock()
+
+ transaction := ContextTransaction(h.context)
+
+ // Initialize the transaction if there's a patch provided in the
+ // context.
+ var patch map[string]interface{}
+ if err := h.context.Get("patch", &patch); err == nil {
+ for key, value := range patch {
+ transaction.Set(h.context.SnapName(), key, value)
+ }
+ }
+
+ return nil
+}
+
+// Done is called by the HookManager after the configure hook has exited
+// successfully.
+func (h *configureHandler) Done() error {
+ return nil
+}
+
+// Error is called by the HookManager after the configure hook has exited
+// non-zero, and includes the error.
+func (h *configureHandler) Error(err error) error {
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type configureHandlerSuite struct {
+ context *hookstate.Context
+ handler hookstate.Handler
+}
+
+var _ = Suite(&configureHandlerSuite{})
+
+func (s *configureHandlerSuite) SetUpTest(c *C) {
+ state := state.New(nil)
+ state.Lock()
+ defer state.Unlock()
+
+ task := state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+
+ var err error
+ s.context, err = hookstate.NewContext(task, setup, hooktest.NewMockHandler())
+ c.Assert(err, IsNil)
+
+ s.handler = configstate.NewConfigureHandler(s.context)
+}
+
+func (s *configureHandlerSuite) TestBeforeInitializesTransaction(c *C) {
+ // Initialize context
+ s.context.Lock()
+ s.context.Set("patch", map[string]interface{}{
+ "foo": "bar",
+ })
+ s.context.Unlock()
+
+ c.Check(s.handler.Before(), IsNil)
+
+ s.context.Lock()
+ transaction := configstate.ContextTransaction(s.context)
+ s.context.Unlock()
+
+ var value string
+ c.Check(transaction.Get("test-snap", "foo", &value), IsNil)
+ c.Check(value, Equals, "bar")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+func init() {
+ snapstate.Configure = Configure
+}
+
+// Configure returns a taskset to apply the given configuration patch.
+func Configure(s *state.State, snapName string, patch map[string]interface{}) *state.TaskSet {
+ hooksup := &hookstate.HookSetup{
+ Snap: snapName,
+ Hook: "configure",
+ Optional: len(patch) == 0,
+ }
+ var contextData map[string]interface{}
+ if len(patch) > 0 {
+ contextData = map[string]interface{}{"patch": patch}
+ }
+ var summary string
+ if hooksup.Optional {
+ summary = fmt.Sprintf(i18n.G("Run configure hook of %q snap if present"), snapName)
+ } else {
+ summary = fmt.Sprintf(i18n.G("Run configure hook of %q snap"), snapName)
+ }
+ task := hookstate.HookTask(s, summary, hooksup, contextData)
+ return state.NewTaskSet(task)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type tasksetsSuite struct {
+ state *state.State
+}
+
+var _ = Suite(&tasksetsSuite{})
+
+func (s *tasksetsSuite) SetUpTest(c *C) {
+ s.state = state.New(nil)
+}
+
+var configureTests = []struct {
+ patch map[string]interface{}
+ optional bool
+}{{
+ patch: nil,
+ optional: true,
+}, {
+ patch: map[string]interface{}{},
+ optional: true,
+}, {
+ patch: map[string]interface{}{"foo": "bar"},
+ optional: false,
+}}
+
+func (s *tasksetsSuite) TestConfigure(c *C) {
+ for _, test := range configureTests {
+ s.state.Lock()
+ taskset := configstate.Configure(s.state, "test-snap", test.patch)
+ s.state.Unlock()
+
+ tasks := taskset.Tasks()
+ c.Assert(tasks, HasLen, 1)
+ task := tasks[0]
+
+ c.Assert(task.Kind(), Equals, "run-hook")
+
+ summary := `Run configure hook of "test-snap" snap`
+ if test.optional {
+ summary += " if present"
+ }
+ c.Assert(task.Summary(), Equals, summary)
+
+ var hooksup hookstate.HookSetup
+ s.state.Lock()
+ err := task.Get("hook-setup", &hooksup)
+ s.state.Unlock()
+ c.Check(err, IsNil)
+
+ c.Assert(hooksup.Snap, Equals, "test-snap")
+ c.Assert(hooksup.Hook, Equals, "configure")
+ c.Assert(hooksup.Optional, Equals, test.optional)
+
+ context, err := hookstate.NewContext(task, &hooksup, nil)
+ c.Check(err, IsNil)
+ c.Check(context.SnapName(), Equals, "test-snap")
+ c.Check(context.SnapRevision(), Equals, snap.Revision{})
+ c.Check(context.HookName(), Equals, "configure")
+
+ var patch map[string]interface{}
+ context.Lock()
+ err = context.Get("patch", &patch)
+ context.Unlock()
+ if len(test.patch) > 0 {
+ c.Check(err, IsNil)
+ c.Check(patch, DeepEquals, test.patch)
+ } else {
+ c.Check(err, Equals, state.ErrNoState)
+ c.Check(patch, IsNil)
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate
+
+import (
+ "encoding/json"
+ "fmt"
+ "regexp"
+ "strings"
+ "sync"
+
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// Transaction holds a copy of the configuration originally present in the
+// provided state which can be queried and mutated in isolation from
+// concurrent logic. All changes performed into it are persisted back into
+// the state at once when Commit is called.
+//
+// Transactions are safe to access and modify concurrently.
+type Transaction struct {
+ mu sync.Mutex
+ state *state.State
+ pristine map[string]map[string]*json.RawMessage // snap => key => value
+ changes map[string]map[string]interface{}
+}
+
+// NewTransaction creates a new configuration transaction initialized with the given state.
+//
+// The provided state must be locked by the caller.
+func NewTransaction(st *state.State) *Transaction {
+ transaction := &Transaction{state: st}
+ transaction.changes = make(map[string]map[string]interface{})
+
+ // Record the current state of the map containing the config of every snap
+ // in the system. We'll use it for this transaction.
+ err := st.Get("config", &transaction.pristine)
+ if err == state.ErrNoState {
+ transaction.pristine = make(map[string]map[string]*json.RawMessage)
+ } else if err != nil {
+ panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err))
+ }
+ return transaction
+}
+
+var validKey = regexp.MustCompile("^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$")
+
+func parseKey(key string) (subkeys []string, err error) {
+ subkeys = strings.Split(key, ".")
+ for _, subkey := range subkeys {
+ if !validKey.MatchString(subkey) {
+ return nil, fmt.Errorf("invalid option name: %q", subkey)
+ }
+ }
+ return subkeys, nil
+}
+
+// Set sets the provided snap's configuration key to the given value.
+// The provided key may be formed as a dotted key path through nested maps.
+// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map.
+// When the key is provided in that form, intermediate maps are mutated
+// rather than replaced, and created when necessary.
+//
+// The provided value must marshal properly by encoding/json.
+// Changes are not persisted until Commit is called.
+func (t *Transaction) Set(snapName, key string, value interface{}) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ config, ok := t.changes[snapName]
+ if !ok {
+ config = make(map[string]interface{})
+ }
+
+ data, err := json.Marshal(value)
+ if err != nil {
+ return fmt.Errorf("cannot marshal snap %q option %q: %s", snapName, key, err)
+ }
+ raw := json.RawMessage(data)
+
+ subkeys, err := parseKey(key)
+ if err != nil {
+ return err
+ }
+
+ // Check whether it's trying to traverse a non-map from pristine. This
+ // would go unperceived by the configuration patching below.
+ if len(subkeys) > 1 {
+ var result interface{}
+ err = getFromPristine(snapName, subkeys, 0, t.pristine[snapName], &result)
+ if err != nil && !IsNoOption(err) {
+ return err
+ }
+ }
+ _, err = patchConfig(snapName, subkeys, 0, config, &raw)
+ if err != nil {
+ return err
+ }
+
+ t.changes[snapName] = config
+ return nil
+}
+
+// Get unmarshals into result the cached value of the provided snap's configuration key.
+// If the key does not exist, an error of type *NoOptionError is returned.
+// The provided key may be formed as a dotted key path through nested maps.
+// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map.
+//
+// Transactions do not see updates from the current state or from other transactions.
+func (t *Transaction) Get(snapName, key string, result interface{}) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ subkeys, err := parseKey(key)
+ if err != nil {
+ return err
+ }
+
+ err = getFromChange(snapName, subkeys, 0, t.changes[snapName], result)
+ if IsNoOption(err) {
+ err = getFromPristine(snapName, subkeys, 0, t.pristine[snapName], result)
+ }
+ return err
+}
+
+// GetMaybe unmarshals into result the cached value of the provided snap's configuration key.
+// If the key does not exist, no error is returned.
+//
+// Transactions do not see updates from the current state or from other transactions.
+func (t *Transaction) GetMaybe(snapName, key string, result interface{}) error {
+ err := t.Get(snapName, key, result)
+ if err != nil && !IsNoOption(err) {
+ return err
+ }
+ return nil
+}
+
+func getFromPristine(snapName string, subkeys []string, pos int, config map[string]*json.RawMessage, result interface{}) error {
+ raw, ok := config[subkeys[pos]]
+ if !ok {
+ return &NoOptionError{SnapName: snapName, Key: strings.Join(subkeys[:pos+1], ".")}
+ }
+
+ if pos+1 == len(subkeys) {
+ err := json.Unmarshal([]byte(*raw), result)
+ if err != nil {
+ key := strings.Join(subkeys, ".")
+ return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", snapName, key, result, err, *raw)
+ }
+ return nil
+ }
+
+ var configm map[string]*json.RawMessage
+ err := json.Unmarshal([]byte(*raw), &configm)
+ if err != nil {
+ return fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos+1], "."))
+ }
+ return getFromPristine(snapName, subkeys, pos+1, configm, result)
+}
+
+func getFromChange(snapName string, subkeys []string, pos int, config map[string]interface{}, result interface{}) error {
+ value, ok := config[subkeys[pos]]
+ if !ok {
+ return &NoOptionError{SnapName: snapName, Key: strings.Join(subkeys[:pos+1], ".")}
+ }
+
+ if pos+1 == len(subkeys) {
+ raw, ok := value.(*json.RawMessage)
+ if !ok {
+ raw = jsonRaw(value)
+ }
+ err := json.Unmarshal([]byte(*raw), result)
+ if err != nil {
+ key := strings.Join(subkeys, ".")
+ return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", snapName, key, result, err, *raw)
+ }
+ return nil
+ }
+
+ configm, ok := value.(map[string]interface{})
+ if !ok {
+ raw, ok := value.(*json.RawMessage)
+ if !ok {
+ raw = jsonRaw(value)
+ }
+ err := json.Unmarshal([]byte(*raw), &configm)
+ if err != nil {
+ return fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos+1], "."))
+ }
+ }
+ return getFromChange(snapName, subkeys, pos+1, configm, result)
+}
+
+func patchConfig(snapName string, subkeys []string, pos int, config interface{}, value *json.RawMessage) (interface{}, error) {
+
+ switch config := config.(type) {
+ case nil:
+ // Missing update map. Create and nest final value under it.
+ configm := make(map[string]interface{})
+ _, err := patchConfig(snapName, subkeys, pos, configm, value)
+ if err != nil {
+ return nil, err
+ }
+ return configm, nil
+
+ case *json.RawMessage:
+ // Raw replaces pristine on commit. Unpack, update, and repack.
+ var configm map[string]interface{}
+ err := json.Unmarshal([]byte(*config), &configm)
+ if err != nil {
+ return nil, fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos], "."))
+ }
+ _, err = patchConfig(snapName, subkeys, pos, configm, value)
+ if err != nil {
+ return nil, err
+ }
+ return jsonRaw(configm), nil
+
+ case map[string]interface{}:
+ // Update map to apply against pristine on commit.
+ if pos+1 == len(subkeys) {
+ config[subkeys[pos]] = value
+ return config, nil
+ } else {
+ result, err := patchConfig(snapName, subkeys, pos+1, config[subkeys[pos]], value)
+ if err != nil {
+ return nil, err
+ }
+ config[subkeys[pos]] = result
+ return config, nil
+ }
+ }
+ panic(fmt.Errorf("internal error: unexpected configuration type %T", config))
+}
+
+// Commit applies to the state the configuration changes made in the transaction
+// and updates the observed configuration to the result of the operation.
+//
+// The state associated with the transaction must be locked by the caller.
+func (t *Transaction) Commit() {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if len(t.changes) == 0 {
+ return
+ }
+
+ // Update our copy of the config with the most recent one from the state.
+ err := t.state.Get("config", &t.pristine)
+ if err == state.ErrNoState {
+ t.pristine = make(map[string]map[string]*json.RawMessage)
+ } else if err != nil {
+ panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err))
+ }
+
+ // Iterate through the write cache and save each item.
+ for snapName, snapChanges := range t.changes {
+ config, ok := t.pristine[snapName]
+ if !ok {
+ config = make(map[string]*json.RawMessage)
+ }
+ for k, v := range snapChanges {
+ config[k] = commitChange(config[k], v)
+ }
+ t.pristine[snapName] = config
+ }
+
+ t.state.Set("config", t.pristine)
+
+ // The cache has been flushed, reset it.
+ t.changes = make(map[string]map[string]interface{})
+}
+
+func jsonRaw(v interface{}) *json.RawMessage {
+ data, err := json.Marshal(v)
+ if err != nil {
+ panic(fmt.Errorf("internal error: cannot marshal configuration: %v", err))
+ }
+ raw := json.RawMessage(data)
+ return &raw
+}
+
+func commitChange(pristine *json.RawMessage, change interface{}) *json.RawMessage {
+ switch change := change.(type) {
+ case *json.RawMessage:
+ return change
+ case map[string]interface{}:
+ if pristine == nil {
+ return jsonRaw(change)
+ }
+ var pristinem map[string]*json.RawMessage
+ if err := json.Unmarshal([]byte(*pristine), &pristinem); err != nil {
+ // Not a map. Overwrite with the change.
+ return jsonRaw(change)
+ }
+ for k, v := range change {
+ pristinem[k] = commitChange(pristinem[k], v)
+ }
+ return jsonRaw(pristinem)
+ }
+ panic(fmt.Errorf("internal error: unexpected configuration type %T", change))
+}
+
+// IsNoOption returns whether the provided error is a *NoOptionError.
+func IsNoOption(err error) bool {
+ _, ok := err.(*NoOptionError)
+ return ok
+}
+
+// NoOptionError indicates that a config option is not set.
+type NoOptionError struct {
+ SnapName string
+ Key string
+}
+
+func (e *NoOptionError) Error() string {
+ return fmt.Sprintf("snap %q has no %q configuration option", e.SnapName, e.Key)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package configstate_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "strings"
+)
+
+func TestConfigState(t *testing.T) { TestingT(t) }
+
+type transactionSuite struct {
+ state *state.State
+ transaction *configstate.Transaction
+}
+
+var _ = Suite(&transactionSuite{})
+
+func (s *transactionSuite) SetUpTest(c *C) {
+ s.state = state.New(nil)
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.transaction = configstate.NewTransaction(s.state)
+}
+
+type setGetOp string
+
+func (op setGetOp) kind() string {
+ return strings.Fields(string(op))[0]
+}
+
+func (op setGetOp) args() map[string]interface{} {
+ m := make(map[string]interface{})
+ args := strings.Fields(string(op))
+ for _, pair := range args[1:] {
+ if pair == "=>" {
+ break
+ }
+ kv := strings.SplitN(pair, "=", 2)
+ var v interface{}
+ err := json.Unmarshal([]byte(kv[1]), &v)
+ if err != nil {
+ v = kv[1]
+ }
+ m[kv[0]] = v
+ }
+ return m
+}
+
+func (op setGetOp) error() string {
+ if i := strings.Index(string(op), " => "); i >= 0 {
+ return string(op[i+4:])
+ }
+ return ""
+}
+
+func (op setGetOp) fails() bool {
+ return op.error() != ""
+}
+
+var setGetTests = [][]setGetOp{{
+ // Basics.
+ `set one=1 two=2`,
+ `setunder three=3`,
+ `get one=1 two=2 three=-`,
+ `getunder one=- two=- three=3`,
+ `commit`,
+ `getunder one=1 two=2 three=3`,
+ `get one=1 two=2 three=3`,
+ `set two=22 four=4`,
+ `get one=1 two=22 three=3 four=4`,
+ `getunder one=1 two=2 three=3 four=-`,
+ `commit`,
+ `getunder one=1 two=22 three=3 four=4`,
+}, {
+ // Trivial full doc.
+ `set doc={"one":1,"two":2}`,
+ `get doc={"one":1,"two":2}`,
+}, {
+ // Nested mutations.
+ `set one.two.three=3`,
+ `set one.five=5`,
+ `setunder one={"two":{"four":4}}`,
+ `get one={"two":{"three":3},"five":5}`,
+ `get one.two={"three":3}`,
+ `get one.two.three=3`,
+ `get one.five=5`,
+ `commit`,
+ `getunder one={"two":{"three":3,"four":4},"five":5}`,
+ `get one={"two":{"three":3,"four":4},"five":5}`,
+ `get one.two={"three":3,"four":4}`,
+ `get one.two.three=3`,
+ `get one.two.four=4`,
+ `get one.five=5`,
+}, {
+ // Replacement with nested mutation.
+ `set one={"two":{"three":3}}`,
+ `set one.five=5`,
+ `get one={"two":{"three":3},"five":5}`,
+ `get one.two={"three":3}`,
+ `get one.two.three=3`,
+ `get one.five=5`,
+ `setunder one={"two":{"four":4},"six":6}`,
+ `commit`,
+ `getunder one={"two":{"three":3},"five":5}`,
+}, {
+ // Cannot go through known scalar implicitly.
+ `set one.two=2`,
+ `set one.two.three=3 => snap "core" option "one\.two" is not a map`,
+ `get one.two.three=3 => snap "core" option "one\.two" is not a map`,
+ `get one={"two":2}`,
+ `commit`,
+ `set one.two.three=3 => snap "core" option "one\.two" is not a map`,
+ `get one.two.three=3 => snap "core" option "one\.two" is not a map`,
+ `get one={"two":2}`,
+ `getunder one={"two":2}`,
+}, {
+ // Unknown scalars may be overwritten though.
+ `setunder one={"two":2}`,
+ `set one.two.three=3`,
+ `commit`,
+ `getunder one={"two":{"three":3}}`,
+}, {
+ // Invalid option names.
+ `set BAD=1 => invalid option name: "BAD"`,
+ `set 42=1 => invalid option name: "42"`,
+ `set .bad=1 => invalid option name: ""`,
+ `set bad.=1 => invalid option name: ""`,
+ `set bad..bad=1 => invalid option name: ""`,
+ `set one.bad--bad.two=1 => invalid option name: "bad--bad"`,
+ `set one.-bad.two=1 => invalid option name: "-bad"`,
+ `set one.bad-.two=1 => invalid option name: "bad-"`,
+}}
+
+func (s *transactionSuite) TestSetGet(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ for _, test := range setGetTests {
+ c.Logf("-----")
+ s.state.Set("config", map[string]interface{}{})
+ t := configstate.NewTransaction(s.state)
+ snap := "core"
+ for _, op := range test {
+ c.Logf("%s", op)
+ switch op.kind() {
+ case "set":
+ for k, v := range op.args() {
+ err := t.Set(snap, k, v)
+ if op.fails() {
+ c.Assert(err, ErrorMatches, op.error())
+ } else {
+ c.Assert(err, IsNil)
+ }
+ }
+
+ case "get":
+ for k, expected := range op.args() {
+ var obtained interface{}
+ err := t.Get(snap, k, &obtained)
+ if op.fails() {
+ c.Assert(err, ErrorMatches, op.error())
+ var nothing interface{}
+ c.Assert(t.GetMaybe(snap, k, ¬hing), ErrorMatches, op.error())
+ c.Assert(nothing, IsNil)
+ continue
+ }
+ if expected == "-" {
+ if !configstate.IsNoOption(err) {
+ c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained)
+ }
+ c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k))
+ var nothing interface{}
+ c.Assert(t.GetMaybe(snap, k, ¬hing), IsNil)
+ c.Assert(nothing, IsNil)
+ continue
+ }
+ c.Assert(err, IsNil)
+ c.Assert(obtained, DeepEquals, expected)
+
+ obtained = nil
+ c.Assert(t.GetMaybe(snap, k, &obtained), IsNil)
+ c.Assert(obtained, DeepEquals, expected)
+ }
+
+ case "commit":
+ t.Commit()
+
+ case "setunder":
+ var config map[string]map[string]interface{}
+ s.state.Get("config", &config)
+ if config == nil {
+ config = make(map[string]map[string]interface{})
+ }
+ if config[snap] == nil {
+ config[snap] = make(map[string]interface{})
+ }
+ for k, v := range op.args() {
+ if v == "-" {
+ delete(config[snap], k)
+ if len(config[snap]) == 0 {
+ delete(config[snap], snap)
+ }
+ } else {
+ config[snap][k] = v
+ }
+ }
+ s.state.Set("config", config)
+
+ case "getunder":
+ var config map[string]map[string]interface{}
+ s.state.Get("config", &config)
+ for k, expected := range op.args() {
+ obtained, ok := config[snap][k]
+ if expected == "-" {
+ if ok {
+ c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained)
+ }
+ continue
+ }
+ c.Assert(obtained, DeepEquals, expected)
+ }
+
+ default:
+ panic("unknown test op kind: " + op.kind())
+ }
+ }
+ }
+}
+
+type brokenType struct {
+ on string
+}
+
+func (b *brokenType) UnmarshalJSON(data []byte) error {
+ if b.on == string(data) {
+ return fmt.Errorf("BAM!")
+ }
+ return nil
+}
+
+func (s *transactionSuite) TestGetUnmarshalError(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil)
+ s.transaction.Commit()
+
+ transaction := configstate.NewTransaction(s.state)
+ c.Check(transaction.Set("test-snap", "foo", "break"), IsNil)
+
+ // Pristine state is good, value in the transaction breaks.
+ broken := brokenType{`"break"`}
+ err := transaction.Get("test-snap", "foo", &broken)
+ c.Assert(err, ErrorMatches, ".*BAM!.*")
+
+ // Pristine state breaks, nothing in the transaction.
+ transaction.Commit()
+ err = transaction.Get("test-snap", "foo", &broken)
+ c.Assert(err, ErrorMatches, ".*BAM!.*")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package devicestate implements the manager and state aspects responsible
+// for the device identity and policies.
+package devicestate
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+// DeviceManager is responsible for managing the device identity and device
+// policies.
+type DeviceManager struct {
+ state *state.State
+ keypairMgr asserts.KeypairManager
+ runner *state.TaskRunner
+ bootOkRan bool
+}
+
+// Manager returns a new device manager.
+func Manager(s *state.State, hookManager *hookstate.HookManager) (*DeviceManager, error) {
+ runner := state.NewTaskRunner(s)
+
+ keypairMgr, err := asserts.OpenFSKeypairManager(dirs.SnapDeviceDir)
+ if err != nil {
+ return nil, err
+
+ }
+
+ m := &DeviceManager{state: s, keypairMgr: keypairMgr, runner: runner}
+
+ hookManager.Register(regexp.MustCompile("^prepare-device$"), newPrepareDeviceHandler)
+
+ runner.AddHandler("generate-device-key", m.doGenerateDeviceKey, nil)
+ runner.AddHandler("request-serial", m.doRequestSerial, nil)
+ runner.AddHandler("mark-seeded", m.doMarkSeeded, nil)
+
+ return m, nil
+}
+
+type prepareDeviceHandler struct{}
+
+func newPrepareDeviceHandler(context *hookstate.Context) hookstate.Handler {
+ return prepareDeviceHandler{}
+}
+
+func (h prepareDeviceHandler) Before() error {
+ return nil
+}
+
+func (h prepareDeviceHandler) Done() error {
+ return nil
+}
+
+func (h prepareDeviceHandler) Error(err error) error {
+ return nil
+}
+
+func (m *DeviceManager) changeInFlight(kind string) bool {
+ for _, chg := range m.state.Changes() {
+ if chg.Kind() == kind && !chg.Status().Ready() {
+ // change already in motion
+ return true
+ }
+ }
+ return false
+}
+
+func (m *DeviceManager) ensureOperational() error {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ device, err := auth.Device(m.state)
+ if err != nil {
+ return err
+ }
+
+ if device.Serial != "" {
+ // serial is set, we are all set
+ return nil
+ }
+
+ if device.Brand == "" || device.Model == "" {
+ // need first-boot, loading of model assertion info
+ if release.OnClassic {
+ // TODO: are we going to have model assertions on classic or need will need to cheat here?
+ return nil
+ }
+ // cannot proceed yet, once first boot is done these will be set
+ // and we can pick up from there
+ return nil
+ }
+
+ if m.changeInFlight("become-operational") {
+ return nil
+ }
+
+ if serialRequestURL == "" {
+ // cannot do anything actually
+ return nil
+ }
+
+ gadgetInfo, err := snapstate.GadgetInfo(m.state)
+ if err == state.ErrNoState {
+ // no gadget installed yet, cannot proceed
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ // XXX: some of these will need to be split and use hooks
+ // retries might need to embrace more than one "task" then,
+ // need to be careful
+
+ tasks := []*state.Task{}
+
+ var prepareDevice *state.Task
+ if gadgetInfo.Hooks["prepare-device"] != nil {
+ summary := i18n.G("Run prepare-device hook")
+ hooksup := &hookstate.HookSetup{
+ Snap: gadgetInfo.Name(),
+ Hook: "prepare-device",
+ }
+ prepareDevice = hookstate.HookTask(m.state, summary, hooksup, nil)
+ tasks = append(tasks, prepareDevice)
+ }
+
+ genKey := m.state.NewTask("generate-device-key", i18n.G("Generate device key"))
+ if prepareDevice != nil {
+ genKey.WaitFor(prepareDevice)
+ }
+ tasks = append(tasks, genKey)
+ requestSerial := m.state.NewTask("request-serial", i18n.G("Request device serial"))
+ requestSerial.WaitFor(genKey)
+ tasks = append(tasks, requestSerial)
+
+ chg := m.state.NewChange("become-operational", i18n.G("Initialize device"))
+ chg.AddAll(state.NewTaskSet(tasks...))
+
+ return nil
+}
+
+var populateStateFromSeed = populateStateFromSeedImpl
+
+// ensureSnaps makes sure that the snaps from seed.yaml get installed
+// with the matching assertions
+func (m *DeviceManager) ensureSeedYaml() error {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ // FIXME: enable on classic?
+ //
+ // Disable seed.yaml on classic for now. In the long run we want
+ // classic to have a seed parsing as well so that we can install
+ // snaps in a classic environment (LP: #1609903). However right
+ // now it is under heavy development so until the dust
+ // settles we disable it.
+ if release.OnClassic {
+ return nil
+ }
+
+ var seeded bool
+ err := m.state.Get("seeded", &seeded)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ if seeded {
+ return nil
+ }
+
+ if m.changeInFlight("seed") {
+ return nil
+ }
+
+ coreInfo, err := snapstate.CoreInfo(m.state)
+ if err == nil && coreInfo.Name() == "ubuntu-core" {
+ // already seeded... recover
+ return m.alreadyFirstbooted()
+ }
+
+ tsAll, err := populateStateFromSeed(m.state)
+ if err != nil {
+ return err
+ }
+ if len(tsAll) == 0 {
+ return nil
+ }
+
+ msg := fmt.Sprintf("Initialize system state")
+ chg := m.state.NewChange("seed", msg)
+ for _, ts := range tsAll {
+ chg.AddAll(ts)
+ }
+ m.state.EnsureBefore(0)
+
+ return nil
+}
+
+// alreadyFirstbooted recovers already first booted devices with the old method appropriately
+func (m *DeviceManager) alreadyFirstbooted() error {
+ device, err := auth.Device(m.state)
+ if err != nil {
+ return err
+ }
+ // recover key-id
+ if device.Brand != "" && device.Model != "" {
+ serials, err := assertstate.DB(m.state).FindMany(asserts.SerialType, map[string]string{
+ "brand-id": device.Brand,
+ "model": device.Model,
+ })
+ if err != nil && err != asserts.ErrNotFound {
+ return err
+ }
+
+ if len(serials) == 1 {
+ // we can recover the key id from the assertion
+ serial := serials[0].(*asserts.Serial)
+ keyID := serial.DeviceKey().ID()
+ device.KeyID = keyID
+ device.Serial = serial.Serial()
+ err := auth.SetDevice(m.state, device)
+ if err != nil {
+ return err
+ }
+ // best effort to cleanup abandoned keys
+ pat := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "*")
+ keyFns, err := filepath.Glob(pat)
+ if err != nil {
+ panic(fmt.Sprintf("invalid glob for device keys: %v", err))
+ }
+ for _, keyFn := range keyFns {
+ if filepath.Base(keyFn) == keyID {
+ continue
+ }
+ os.Remove(keyFn)
+ }
+ }
+
+ }
+
+ m.state.Set("seeded", true)
+ return nil
+}
+
+func (m *DeviceManager) ensureBootOk() error {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ if release.OnClassic {
+ return nil
+ }
+
+ if !m.bootOkRan {
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot mark boot successful: %s"), err)
+ }
+ if err := partition.MarkBootSuccessful(bootloader); err != nil {
+ return err
+ }
+ m.bootOkRan = true
+ }
+
+ return snapstate.UpdateBootRevisions(m.state)
+}
+
+type ensureError struct {
+ errs []error
+}
+
+func (e *ensureError) Error() string {
+ if len(e.errs) == 1 {
+ return fmt.Sprintf("devicemgr: %v", e.errs[0])
+ }
+ parts := []string{"devicemgr:"}
+ for _, e := range e.errs {
+ parts = append(parts, e.Error())
+ }
+ return strings.Join(parts, "\n - ")
+}
+
+// Ensure implements StateManager.Ensure.
+func (m *DeviceManager) Ensure() error {
+ var errs []error
+
+ if err := m.ensureSeedYaml(); err != nil {
+ errs = append(errs, err)
+ }
+ if err := m.ensureOperational(); err != nil {
+ errs = append(errs, err)
+ }
+
+ if err := m.ensureBootOk(); err != nil {
+ errs = append(errs, err)
+ }
+
+ m.runner.Ensure()
+
+ if len(errs) > 0 {
+ return &ensureError{errs}
+ }
+
+ return nil
+}
+
+// Wait implements StateManager.Wait.
+func (m *DeviceManager) Wait() {
+ m.runner.Wait()
+}
+
+// Stop implements StateManager.Stop.
+func (m *DeviceManager) Stop() {
+ m.runner.Stop()
+}
+
+func useStaging() bool {
+ return osutil.GetenvBool("SNAPPY_USE_STAGING_STORE")
+}
+
+func deviceAPIBaseURL() string {
+ if useStaging() {
+ return "https://myapps.developer.staging.ubuntu.com/identity/api/v1/"
+ }
+ return "https://myapps.developer.ubuntu.com/identity/api/v1/"
+}
+
+var (
+ keyLength = 4096
+ retryInterval = 60 * time.Second
+ deviceAPIBase = deviceAPIBaseURL()
+ requestIDURL = deviceAPIBase + "request-id"
+ serialRequestURL = deviceAPIBase + "devices"
+)
+
+func (m *DeviceManager) doGenerateDeviceKey(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+
+ device, err := auth.Device(st)
+ if err != nil {
+ return err
+ }
+
+ if device.KeyID != "" {
+ // nothing to do
+ return nil
+ }
+
+ keyPair, err := rsa.GenerateKey(rand.Reader, keyLength)
+ if err != nil {
+ return fmt.Errorf("cannot generate device key pair: %v", err)
+ }
+
+ privKey := asserts.RSAPrivateKey(keyPair)
+ err = m.keypairMgr.Put(privKey)
+ if err != nil {
+ return fmt.Errorf("cannot store device key pair: %v", err)
+ }
+
+ device.KeyID = privKey.PublicKey().ID()
+ err = auth.SetDevice(st, device)
+ if err != nil {
+ return err
+ }
+ t.SetStatus(state.DoneStatus)
+ return nil
+}
+
+func (m *DeviceManager) keyPair() (asserts.PrivateKey, error) {
+ device, err := auth.Device(m.state)
+ if err != nil {
+ return nil, err
+ }
+
+ if device.KeyID == "" {
+ return nil, state.ErrNoState
+ }
+
+ privKey, err := m.keypairMgr.Get(device.KeyID)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read device key pair: %v", err)
+ }
+ return privKey, nil
+}
+
+type serialSetup struct {
+ SerialRequest string `json:"serial-request"`
+ Serial string `json:"serial"`
+}
+
+type requestIDResp struct {
+ RequestID string `json:"request-id"`
+}
+
+func retryErr(t *state.Task, reason string, a ...interface{}) error {
+ t.State().Lock()
+ defer t.State().Unlock()
+ t.Errorf(reason, a...)
+ return &state.Retry{After: retryInterval}
+}
+
+func prepareSerialRequest(t *state.Task, privKey asserts.PrivateKey, device *auth.DeviceState, client *http.Client, cfg *serialRequestConfig) (string, error) {
+ st := t.State()
+ st.Unlock()
+ defer st.Lock()
+
+ req, err := http.NewRequest("POST", cfg.requestIDURL, nil)
+ if err != nil {
+ return "", fmt.Errorf("internal error: cannot create request-id request %q", cfg.requestIDURL)
+ }
+ cfg.applyHeaders(req)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", retryErr(t, "cannot retrieve request-id for making a request for a serial: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ return "", retryErr(t, "cannot retrieve request-id for making a request for a serial: unexpected status %d", resp.StatusCode)
+ }
+
+ dec := json.NewDecoder(resp.Body)
+ var requestID requestIDResp
+ err = dec.Decode(&requestID)
+ if err != nil { // assume broken i/o
+ return "", retryErr(t, "cannot read response with request-id for making a request for a serial: %v", err)
+ }
+
+ encodedPubKey, err := asserts.EncodePublicKey(privKey.PublicKey())
+ if err != nil {
+ return "", fmt.Errorf("internal error: cannot encode device public key: %v", err)
+
+ }
+
+ headers := map[string]interface{}{
+ "brand-id": device.Brand,
+ "model": device.Model,
+ "request-id": requestID.RequestID,
+ "device-key": string(encodedPubKey),
+ }
+ if cfg.proposedSerial != "" {
+ headers["serial"] = cfg.proposedSerial
+ }
+
+ serialReq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType, headers, cfg.body, privKey)
+ if err != nil {
+ return "", err
+ }
+
+ return string(asserts.Encode(serialReq)), nil
+}
+
+var errPoll = errors.New("serial-request accepted, poll later")
+
+func submitSerialRequest(t *state.Task, serialRequest string, client *http.Client, cfg *serialRequestConfig) (*asserts.Serial, error) {
+ st := t.State()
+ st.Unlock()
+ defer st.Lock()
+
+ req, err := http.NewRequest("POST", cfg.serialRequestURL, bytes.NewBufferString(serialRequest))
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot create serial-request request %q", cfg.serialRequestURL)
+ }
+ cfg.applyHeaders(req)
+ req.Header.Set("Content-Type", asserts.MediaType)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, retryErr(t, "cannot deliver device serial request: %v", err)
+ }
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case 200, 201:
+ case 202:
+ return nil, errPoll
+ default:
+ return nil, retryErr(t, "cannot deliver device serial request: unexpected status %d", resp.StatusCode)
+ }
+
+ // decode body with serial assertion
+ dec := asserts.NewDecoder(resp.Body)
+ got, err := dec.Decode()
+ if err != nil { // assume broken i/o
+ return nil, retryErr(t, "cannot read response to request for a serial: %v", err)
+ }
+
+ serial, ok := got.(*asserts.Serial)
+ if !ok {
+ return nil, fmt.Errorf("cannot use device serial assertion of type %q", got.Type().Name)
+ }
+
+ return serial, nil
+}
+
+func getSerial(t *state.Task, privKey asserts.PrivateKey, device *auth.DeviceState, cfg *serialRequestConfig) (*asserts.Serial, error) {
+ var serialSup serialSetup
+ err := t.Get("serial-setup", &serialSup)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+
+ if serialSup.Serial != "" {
+ // we got a serial, just haven't managed to save its info yet
+ a, err := asserts.Decode([]byte(serialSup.Serial))
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot decode previously saved serial: %v", err)
+ }
+ return a.(*asserts.Serial), nil
+ }
+
+ client := &http.Client{Timeout: 30 * time.Second}
+
+ // NB: until we get at least an Accepted (202) we need to
+ // retry from scratch creating a new request-id because the
+ // previous one used could have expired
+
+ if serialSup.SerialRequest == "" {
+ serialRequest, err := prepareSerialRequest(t, privKey, device, client, cfg)
+ if err != nil { // errors & retries
+ return nil, err
+ }
+
+ serialSup.SerialRequest = serialRequest
+ }
+
+ serial, err := submitSerialRequest(t, serialSup.SerialRequest, client, cfg)
+ if err == errPoll {
+ // we can/should reuse the serial-request
+ t.Set("serial-setup", serialSup)
+ return nil, errPoll
+ }
+ if err != nil { // errors & retries
+ return nil, err
+ }
+
+ keyID := privKey.PublicKey().ID()
+ if serial.BrandID() != device.Brand || serial.Model() != device.Model || serial.DeviceKey().ID() != keyID {
+ return nil, fmt.Errorf("obtained serial assertion does not match provided device identity information (brand, model, key id): %s / %s / %s != %s / %s / %s", serial.BrandID(), serial.Model(), serial.DeviceKey().ID(), device.Brand, device.Model, keyID)
+ }
+
+ serialSup.Serial = string(asserts.Encode(serial))
+ t.Set("serial-setup", serialSup)
+
+ if repeatRequestSerial == "after-got-serial" {
+ // For testing purposes, ensure a crash in this state works.
+ return nil, &state.Retry{}
+ }
+
+ return serial, nil
+}
+
+type serialRequestConfig struct {
+ requestIDURL string
+ serialRequestURL string
+ headers map[string]string
+ proposedSerial string
+ body []byte
+}
+
+func (cfg *serialRequestConfig) applyHeaders(req *http.Request) {
+ for k, v := range cfg.headers {
+ req.Header.Set(k, v)
+ }
+}
+
+func getSerialRequestConfig(t *state.Task) (*serialRequestConfig, error) {
+ gadgetInfo, err := snapstate.GadgetInfo(t.State())
+ if err != nil {
+ return nil, fmt.Errorf("cannot find gadget snap and its name: %v", err)
+ }
+ gadgetName := gadgetInfo.Name()
+
+ tr := configstate.NewTransaction(t.State())
+ var svcURL string
+ err = tr.GetMaybe(gadgetName, "device-service.url", &svcURL)
+ if err != nil {
+ return nil, err
+ }
+
+ if svcURL != "" {
+ baseURL, err := url.Parse(svcURL)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse device registration base URL %q: %v", svcURL, err)
+ }
+
+ var headers map[string]string
+ err = tr.GetMaybe(gadgetName, "device-service.headers", &headers)
+ if err != nil {
+ return nil, err
+ }
+
+ cfg := serialRequestConfig{
+ headers: headers,
+ }
+
+ reqIDURL, err := baseURL.Parse("request-id")
+ if err != nil {
+ return nil, fmt.Errorf("cannot build /request-id URL from %v: %v", baseURL, err)
+ }
+ cfg.requestIDURL = reqIDURL.String()
+
+ var bodyStr string
+ err = tr.GetMaybe(gadgetName, "registration.body", &bodyStr)
+ if err != nil {
+ return nil, err
+ }
+
+ cfg.body = []byte(bodyStr)
+
+ serialURL, err := baseURL.Parse("serial")
+ if err != nil {
+ return nil, fmt.Errorf("cannot build /serial URL from %v: %v", baseURL, err)
+ }
+ cfg.serialRequestURL = serialURL.String()
+
+ var proposedSerial string
+ err = tr.GetMaybe(gadgetName, "registration.proposed-serial", &proposedSerial)
+ if err != nil {
+ return nil, err
+ }
+ cfg.proposedSerial = proposedSerial
+
+ return &cfg, nil
+ }
+
+ return &serialRequestConfig{
+ requestIDURL: requestIDURL,
+ serialRequestURL: serialRequestURL,
+ }, nil
+}
+
+func (m *DeviceManager) doRequestSerial(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+
+ cfg, err := getSerialRequestConfig(t)
+ if err != nil {
+ return err
+ }
+
+ device, err := auth.Device(st)
+ if err != nil {
+ return err
+ }
+
+ privKey, err := m.keyPair()
+ if err == state.ErrNoState {
+ return fmt.Errorf("internal error: cannot find device key pair")
+ }
+ if err != nil {
+ return err
+ }
+
+ // make this idempotent, look if we have already a serial assertion
+ // for privKey
+ serials, err := assertstate.DB(st).FindMany(asserts.SerialType, map[string]string{
+ "brand-id": device.Brand,
+ "model": device.Model,
+ "device-key-sha3-384": privKey.PublicKey().ID(),
+ })
+ if err != nil && err != asserts.ErrNotFound {
+ return err
+ }
+
+ if len(serials) == 1 {
+ // means we saved the assertion but didn't get to the end of the task
+ device.Serial = serials[0].(*asserts.Serial).Serial()
+ err := auth.SetDevice(st, device)
+ if err != nil {
+ return err
+ }
+ t.SetStatus(state.DoneStatus)
+ return nil
+ }
+ if len(serials) > 1 {
+ return fmt.Errorf("internal error: multiple serial assertions for the same device key")
+ }
+
+ serial, err := getSerial(t, privKey, device, cfg)
+ if err == errPoll {
+ t.Logf("Will poll for device serial assertion in 60 seconds")
+ return &state.Retry{After: retryInterval}
+ }
+ if err != nil { // errors & retries
+ return err
+ }
+
+ sto := snapstate.Store(st)
+ // try to fetch the signing key of the serial
+ st.Unlock()
+ a, errAcctKey := sto.Assertion(asserts.AccountKeyType, []string{serial.SignKeyID()}, nil)
+ st.Lock()
+ if errAcctKey == nil {
+ err := assertstate.Add(st, a)
+ if err != nil {
+ if !asserts.IsUnaccceptedUpdate(err) {
+ return err
+ }
+ }
+ }
+
+ // add the serial assertion to the system assertion db
+ err = assertstate.Add(st, serial)
+ if err != nil {
+ // if we had failed to fetch the signing key, retry in a bit
+ if errAcctKey != nil {
+ t.Errorf("cannot fetch signing key for the serial: %v", errAcctKey)
+ return &state.Retry{After: retryInterval}
+ }
+ return err
+ }
+
+ if repeatRequestSerial == "after-add-serial" {
+ // For testing purposes, ensure a crash in this state works.
+ return &state.Retry{}
+ }
+
+ device.Serial = serial.Serial()
+ err = auth.SetDevice(st, device)
+ if err != nil {
+ return err
+ }
+ t.SetStatus(state.DoneStatus)
+ return nil
+}
+
+func (m *DeviceManager) doMarkSeeded(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+
+ st.Set("seeded", true)
+ return nil
+}
+
+var repeatRequestSerial string
+
+// implementing auth.DeviceAssertions
+// sanity check
+var _ auth.DeviceAssertions = (*DeviceManager)(nil)
+
+// Model returns the device model assertion.
+func (m *DeviceManager) Model() (*asserts.Model, error) {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ return Model(m.state)
+}
+
+// Serial returns the device serial assertion.
+func (m *DeviceManager) Serial() (*asserts.Serial, error) {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ return Serial(m.state)
+}
+
+// DeviceSessionRequest produces a device-session-request with the given nonce, it also returns the device serial assertion.
+func (m *DeviceManager) DeviceSessionRequest(nonce string) (*asserts.DeviceSessionRequest, *asserts.Serial, error) {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ serial, err := Serial(m.state)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ privKey, err := m.keyPair()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ a, err := asserts.SignWithoutAuthority(asserts.DeviceSessionRequestType, map[string]interface{}{
+ "brand-id": serial.BrandID(),
+ "model": serial.Model(),
+ "serial": serial.Serial(),
+ "nonce": nonce,
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, privKey)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return a.(*asserts.DeviceSessionRequest), serial, err
+
+}
+
+// Model returns the device model assertion.
+func Model(st *state.State) (*asserts.Model, error) {
+ device, err := auth.Device(st)
+ if err != nil {
+ return nil, err
+ }
+
+ if device.Brand == "" || device.Model == "" {
+ return nil, state.ErrNoState
+ }
+
+ a, err := assertstate.DB(st).Find(asserts.ModelType, map[string]string{
+ "series": release.Series,
+ "brand-id": device.Brand,
+ "model": device.Model,
+ })
+ if err == asserts.ErrNotFound {
+ return nil, state.ErrNoState
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return a.(*asserts.Model), nil
+}
+
+// Serial returns the device serial assertion.
+func Serial(st *state.State) (*asserts.Serial, error) {
+ device, err := auth.Device(st)
+ if err != nil {
+ return nil, err
+ }
+
+ if device.Serial == "" {
+ return nil, state.ErrNoState
+ }
+
+ a, err := assertstate.DB(st).Find(asserts.SerialType, map[string]string{
+ "brand-id": device.Brand,
+ "model": device.Model,
+ "serial": device.Serial,
+ })
+ if err == asserts.ErrNotFound {
+ return nil, state.ErrNoState
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return a.(*asserts.Serial), nil
+}
+
+func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags snapstate.Flags) error {
+ kind := ""
+ var currentInfo func(*state.State) (*snap.Info, error)
+ var getName func(*asserts.Model) string
+ switch snapInfo.Type {
+ case snap.TypeGadget:
+ kind = "gadget"
+ currentInfo = snapstate.GadgetInfo
+ getName = (*asserts.Model).Gadget
+ case snap.TypeKernel:
+ kind = "kernel"
+ currentInfo = snapstate.KernelInfo
+ getName = (*asserts.Model).Kernel
+ default:
+ // not a relevant check
+ return nil
+ }
+
+ if release.OnClassic {
+ // for the time being
+ return fmt.Errorf("cannot install a %s snap on classic", kind)
+ }
+
+ currentSnap, err := currentInfo(st)
+ if err != nil && err != state.ErrNoState {
+ return fmt.Errorf("cannot find original %s snap: %v", kind, err)
+ }
+ if currentSnap != nil {
+ // already installed, snapstate takes care
+ return nil
+ }
+ // first installation of a gadget/kernel
+
+ model, err := Model(st)
+ if err == state.ErrNoState {
+ return fmt.Errorf("cannot install %s without model assertion", kind)
+ }
+ if err != nil {
+ return err
+ }
+
+ expectedName := getName(model)
+ if snapInfo.Name() != expectedName {
+ return fmt.Errorf("cannot install %s %q, model assertion requests %q", kind, snapInfo.Name(), expectedName)
+ }
+
+ return nil
+}
+
+func init() {
+ snapstate.AddCheckSnapCallback(checkGadgetOrKernel)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package devicestate_test
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "sync"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+ . "gopkg.in/check.v1"
+ "gopkg.in/tomb.v2"
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/store"
+)
+
+func TestDeviceManager(t *testing.T) { TestingT(t) }
+
+type deviceMgrSuite struct {
+ state *state.State
+ hookMgr *hookstate.HookManager
+ mgr *devicestate.DeviceManager
+ db *asserts.Database
+
+ storeSigning *assertstest.StoreStack
+}
+
+var _ = Suite(&deviceMgrSuite{})
+
+type fakeStore struct {
+ state *state.State
+ db asserts.RODatabase
+}
+
+func (sto *fakeStore) pokeStateLock() {
+ // the store should be called without the state lock held. Try
+ // to acquire it.
+ sto.state.Lock()
+ sto.state.Unlock()
+}
+
+func (sto *fakeStore) Assertion(assertType *asserts.AssertionType, key []string, _ *auth.UserState) (asserts.Assertion, error) {
+ sto.pokeStateLock()
+ ref := &asserts.Ref{Type: assertType, PrimaryKey: key}
+ a, err := ref.Resolve(sto.db.Find)
+ if err != nil {
+ return nil, &store.AssertionNotFoundError{Ref: ref}
+ }
+ return a, nil
+}
+
+func (*fakeStore) SnapInfo(store.SnapSpec, *auth.UserState) (*snap.Info, error) {
+ panic("fakeStore.SnapInfo not expected")
+}
+
+func (sto *fakeStore) Find(*store.Search, *auth.UserState) ([]*snap.Info, error) {
+ panic("fakeStore.Find not expected")
+}
+
+func (sto *fakeStore) ListRefresh([]*store.RefreshCandidate, *auth.UserState) ([]*snap.Info, error) {
+ panic("fakeStore.ListRefresh not expected")
+}
+
+func (sto *fakeStore) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error {
+ panic("fakeStore.Download not expected")
+}
+
+func (sto *fakeStore) SuggestedCurrency() string {
+ panic("fakeStore.SuggestedCurrency not expected")
+}
+
+func (sto *fakeStore) Buy(*store.BuyOptions, *auth.UserState) (*store.BuyResult, error) {
+ panic("fakeStore.Buy not expected")
+}
+
+func (sto *fakeStore) ReadyToBuy(*auth.UserState) error {
+ panic("fakeStore.ReadyToBuy not expected")
+}
+
+func (sto *fakeStore) Sections(*auth.UserState) ([]string, error) {
+ panic("fakeStore.Sections not expected")
+}
+
+func (s *deviceMgrSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ s.storeSigning = assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey)
+ s.state = state.New(nil)
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.storeSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ assertstate.ReplaceDB(s.state, db)
+ s.state.Unlock()
+
+ err = db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ hookMgr, err := hookstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ mgr, err := devicestate.Manager(s.state, hookMgr)
+ c.Assert(err, IsNil)
+
+ s.db = db
+ s.hookMgr = hookMgr
+ s.mgr = mgr
+
+ s.state.Lock()
+ snapstate.ReplaceStore(s.state, &fakeStore{
+ state: s.state,
+ db: s.storeSigning,
+ })
+ s.state.Unlock()
+}
+
+func (s *deviceMgrSuite) TearDownTest(c *C) {
+ s.state.Lock()
+ assertstate.ReplaceDB(s.state, nil)
+ s.state.Unlock()
+ dirs.SetRootDir("")
+ release.OnClassic = true
+}
+
+func (s *deviceMgrSuite) settle() {
+ for i := 0; i < 50; i++ {
+ s.hookMgr.Ensure()
+ s.mgr.Ensure()
+ s.hookMgr.Wait()
+ s.mgr.Wait()
+ }
+}
+
+func (s *deviceMgrSuite) mockServer(c *C, reqID string) *httptest.Server {
+ var mu sync.Mutex
+ count := 0
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/identity/api/v1/request-id":
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, fmt.Sprintf(`{"request-id": "%s"}`, reqID))
+
+ case "/identity/api/v1/serial":
+ c.Check(r.Header.Get("X-Extra-Header"), Equals, "extra")
+ fallthrough
+ case "/identity/api/v1/devices":
+ mu.Lock()
+ serialNum := 9999 + count
+ count++
+ mu.Unlock()
+
+ b, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ a, err := asserts.Decode(b)
+ c.Assert(err, IsNil)
+ serialReq, ok := a.(*asserts.SerialRequest)
+ c.Assert(ok, Equals, true)
+ err = asserts.SignatureCheck(serialReq, serialReq.DeviceKey())
+ c.Assert(err, IsNil)
+ c.Check(serialReq.BrandID(), Equals, "canonical")
+ c.Check(serialReq.Model(), Equals, "pc")
+ if reqID == "REQID-POLL" && serialNum != 10002 {
+ w.WriteHeader(http.StatusAccepted)
+ return
+ }
+ serialStr := fmt.Sprintf("%d", serialNum)
+ if serialReq.Serial() != "" {
+ // use proposed serial
+ serialStr = serialReq.Serial()
+ }
+ serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": serialStr,
+ "device-key": serialReq.HeaderString("device-key"),
+ "device-key-sha3-384": serialReq.SignKeyID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, serialReq.Body(), "")
+ c.Assert(err, IsNil)
+ w.Header().Set("Content-Type", asserts.MediaType)
+ w.WriteHeader(http.StatusOK)
+ w.Write(asserts.Encode(serial))
+ }
+ }))
+}
+
+func (s *deviceMgrSuite) setupGadget(c *C, snapYaml string, snapContents string) {
+ sideInfoGadget := &snap.SideInfo{
+ RealName: "gadget",
+ Revision: snap.R(2),
+ }
+ snaptest.MockSnap(c, snapYaml, snapContents, sideInfoGadget)
+ snapstate.Set(s.state, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfoGadget},
+ Current: sideInfoGadget.Revision,
+ })
+}
+
+func (s *deviceMgrSuite) setupCore(c *C, name, snapYaml string, snapContents string) {
+ sideInfoCore := &snap.SideInfo{
+ RealName: name,
+ Revision: snap.R(3),
+ }
+ snaptest.MockSnap(c, snapYaml, snapContents, sideInfoCore)
+ snapstate.Set(s.state, name, &snapstate.SnapState{
+ SnapType: "os",
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfoCore},
+ Current: sideInfoCore.Revision,
+ })
+}
+
+func (s *deviceMgrSuite) TestFullDeviceRegistrationHappy(c *C) {
+ r1 := devicestate.MockKeyLength(752)
+ defer r1()
+
+ mockServer := s.mockServer(c, "REQID-1")
+ defer mockServer.Close()
+
+ mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id"
+ r2 := devicestate.MockRequestIDURL(mockRequestIDURL)
+ defer r2()
+
+ mockSerialRequestURL := mockServer.URL + "/identity/api/v1/devices"
+ r3 := devicestate.MockSerialRequestURL(mockSerialRequestURL)
+ defer r3()
+
+ // setup state as will be done by first-boot
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.setupGadget(c, `
+name: gadget
+type: gadget
+version: gadget
+`, "")
+
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+
+ // runs the whole device registration process
+ s.state.Unlock()
+ s.settle()
+ s.state.Lock()
+
+ var becomeOperational *state.Change
+ for _, chg := range s.state.Changes() {
+ if chg.Kind() == "become-operational" {
+ becomeOperational = chg
+ break
+ }
+ }
+ c.Assert(becomeOperational, NotNil)
+
+ c.Check(becomeOperational.Status().Ready(), Equals, true)
+ c.Check(becomeOperational.Err(), IsNil)
+
+ device, err := auth.Device(s.state)
+ c.Assert(err, IsNil)
+ c.Check(device.Brand, Equals, "canonical")
+ c.Check(device.Model, Equals, "pc")
+ c.Check(device.Serial, Equals, "9999")
+
+ a, err := s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "9999",
+ })
+ c.Assert(err, IsNil)
+ serial := a.(*asserts.Serial)
+
+ privKey, err := s.mgr.KeypairManager().Get(serial.DeviceKey().ID())
+ c.Assert(err, IsNil)
+ c.Check(privKey, NotNil)
+
+ c.Check(device.KeyID, Equals, privKey.PublicKey().ID())
+}
+
+func (s *deviceMgrSuite) TestDoRequestSerialIdempotentAfterAddSerial(c *C) {
+ privKey, _ := assertstest.GenerateKey(1024)
+
+ mockServer := s.mockServer(c, "REQID-1")
+ defer mockServer.Close()
+
+ mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id"
+ restore := devicestate.MockRequestIDURL(mockRequestIDURL)
+ defer restore()
+
+ mockSerialRequestURL := mockServer.URL + "/identity/api/v1/devices"
+ restore = devicestate.MockSerialRequestURL(mockSerialRequestURL)
+ defer restore()
+
+ restore = devicestate.MockRepeatRequestSerial("after-add-serial")
+ defer restore()
+
+ // setup state as done by first-boot/Ensure/doGenerateDeviceKey
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.setupGadget(c, `
+name: gadget
+type: gadget
+version: gadget
+`, "")
+
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ KeyID: privKey.PublicKey().ID(),
+ })
+ s.mgr.KeypairManager().Put(privKey)
+
+ t := s.state.NewTask("request-serial", "test")
+ chg := s.state.NewChange("become-operational", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+ s.mgr.Ensure()
+ s.mgr.Wait()
+ s.state.Lock()
+
+ c.Check(chg.Status(), Equals, state.DoingStatus)
+ device, err := auth.Device(s.state)
+ c.Check(err, IsNil)
+ _, err = s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "9999",
+ })
+ c.Assert(err, IsNil)
+
+ s.state.Unlock()
+ s.mgr.Ensure()
+ s.mgr.Wait()
+ s.state.Lock()
+
+ // Repeated handler run but set original serial.
+ c.Check(chg.Status(), Equals, state.DoneStatus)
+ device, err = auth.Device(s.state)
+ c.Check(err, IsNil)
+ c.Check(device.Serial, Equals, "9999")
+}
+
+func (s *deviceMgrSuite) TestDoRequestSerialIdempotentAfterGotSerial(c *C) {
+ privKey, _ := assertstest.GenerateKey(1024)
+
+ mockServer := s.mockServer(c, "REQID-1")
+ defer mockServer.Close()
+
+ mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id"
+ restore := devicestate.MockRequestIDURL(mockRequestIDURL)
+ defer restore()
+
+ mockSerialRequestURL := mockServer.URL + "/identity/api/v1/devices"
+ restore = devicestate.MockSerialRequestURL(mockSerialRequestURL)
+ defer restore()
+
+ restore = devicestate.MockRepeatRequestSerial("after-got-serial")
+ defer restore()
+
+ // setup state as done by first-boot/Ensure/doGenerateDeviceKey
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.setupGadget(c, `
+name: gadget
+type: gadget
+version: gadget
+`, "")
+
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ KeyID: privKey.PublicKey().ID(),
+ })
+ s.mgr.KeypairManager().Put(privKey)
+
+ t := s.state.NewTask("request-serial", "test")
+ chg := s.state.NewChange("become-operational", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+ s.mgr.Ensure()
+ s.mgr.Wait()
+ s.state.Lock()
+
+ c.Check(chg.Status(), Equals, state.DoingStatus)
+ device, err := auth.Device(s.state)
+ c.Check(err, IsNil)
+ _, err = s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "9999",
+ })
+ c.Assert(err, Equals, asserts.ErrNotFound)
+
+ s.state.Unlock()
+ s.mgr.Ensure()
+ s.mgr.Wait()
+ s.state.Lock()
+
+ // Repeated handler run but set original serial.
+ c.Check(chg.Status(), Equals, state.DoneStatus)
+ device, err = auth.Device(s.state)
+ c.Check(err, IsNil)
+ c.Check(device.Serial, Equals, "9999")
+}
+
+func (s *deviceMgrSuite) TestFullDeviceRegistrationPollHappy(c *C) {
+ r1 := devicestate.MockKeyLength(752)
+ defer r1()
+
+ mockServer := s.mockServer(c, "REQID-POLL")
+ defer mockServer.Close()
+
+ mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id"
+ r2 := devicestate.MockRequestIDURL(mockRequestIDURL)
+ defer r2()
+
+ mockSerialRequestURL := mockServer.URL + "/identity/api/v1/devices"
+ r3 := devicestate.MockSerialRequestURL(mockSerialRequestURL)
+ defer r3()
+
+ // immediately
+ r4 := devicestate.MockRetryInterval(0)
+ defer r4()
+
+ // setup state as will be done by first-boot
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.setupGadget(c, `
+name: gadget
+type: gadget
+version: gadget
+`, "")
+
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+
+ // runs the whole device registration process with polling
+ s.state.Unlock()
+ s.settle()
+ s.state.Lock()
+
+ var becomeOperational *state.Change
+ for _, chg := range s.state.Changes() {
+ if chg.Kind() == "become-operational" {
+ becomeOperational = chg
+ break
+ }
+ }
+ c.Assert(becomeOperational, NotNil)
+
+ c.Check(becomeOperational.Status().Ready(), Equals, true)
+ c.Check(becomeOperational.Err(), IsNil)
+
+ device, err := auth.Device(s.state)
+ c.Assert(err, IsNil)
+ c.Check(device.Brand, Equals, "canonical")
+ c.Check(device.Model, Equals, "pc")
+ c.Check(device.Serial, Equals, "10002")
+
+ a, err := s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "10002",
+ })
+ c.Assert(err, IsNil)
+ serial := a.(*asserts.Serial)
+
+ privKey, err := s.mgr.KeypairManager().Get(serial.DeviceKey().ID())
+ c.Assert(err, IsNil)
+ c.Check(privKey, NotNil)
+
+ c.Check(device.KeyID, Equals, privKey.PublicKey().ID())
+}
+
+func (s *deviceMgrSuite) TestFullDeviceRegistrationHappyPrepareDeviceHook(c *C) {
+ r1 := devicestate.MockKeyLength(752)
+ defer r1()
+
+ mockServer := s.mockServer(c, "REQID-1")
+ defer mockServer.Close()
+
+ r2 := hookstate.MockRunHook(func(ctx *hookstate.Context, _ *tomb.Tomb) ([]byte, error) {
+ c.Assert(ctx.HookName(), Equals, "prepare-device")
+
+ // snapctl set the registration params
+ _, _, err := ctlcmd.Run(ctx, []string{"set", fmt.Sprintf("device-service.url=%q", mockServer.URL+"/identity/api/v1/")})
+ c.Assert(err, IsNil)
+
+ h, err := json.Marshal(map[string]string{
+ "x-extra-header": "extra",
+ })
+ c.Assert(err, IsNil)
+ _, _, err = ctlcmd.Run(ctx, []string{"set", fmt.Sprintf("device-service.headers=%s", string(h))})
+ c.Assert(err, IsNil)
+
+ _, _, err = ctlcmd.Run(ctx, []string{"set", fmt.Sprintf("registration.proposed-serial=%q", "Y9999")})
+ c.Assert(err, IsNil)
+
+ d, err := yaml.Marshal(map[string]string{
+ "mac": "00:00:00:00:ff:00",
+ })
+ c.Assert(err, IsNil)
+ _, _, err = ctlcmd.Run(ctx, []string{"set", fmt.Sprintf("registration.body=%q", d)})
+ c.Assert(err, IsNil)
+
+ return nil, nil
+ })
+ defer r2()
+
+ // setup state as will be done by first-boot
+ // & have a gadget with a prepare-device hook
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.setupGadget(c, `
+name: gadget
+type: gadget
+version: gadget
+hooks:
+ prepare-device:
+`, "")
+
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+
+ // runs the whole device registration process
+ s.state.Unlock()
+ s.settle()
+ s.state.Lock()
+
+ var becomeOperational *state.Change
+ for _, chg := range s.state.Changes() {
+ if chg.Kind() == "become-operational" {
+ becomeOperational = chg
+ break
+ }
+ }
+ c.Assert(becomeOperational, NotNil)
+
+ c.Check(becomeOperational.Status().Ready(), Equals, true)
+ c.Check(becomeOperational.Err(), IsNil)
+
+ device, err := auth.Device(s.state)
+ c.Assert(err, IsNil)
+ c.Check(device.Brand, Equals, "canonical")
+ c.Check(device.Model, Equals, "pc")
+ c.Check(device.Serial, Equals, "Y9999")
+
+ a, err := s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "Y9999",
+ })
+ c.Assert(err, IsNil)
+ serial := a.(*asserts.Serial)
+
+ var details map[string]interface{}
+ err = yaml.Unmarshal(serial.Body(), &details)
+ c.Assert(err, IsNil)
+
+ c.Check(details, DeepEquals, map[string]interface{}{
+ "mac": "00:00:00:00:ff:00",
+ })
+
+ privKey, err := s.mgr.KeypairManager().Get(serial.DeviceKey().ID())
+ c.Assert(err, IsNil)
+ c.Check(privKey, NotNil)
+
+ c.Check(device.KeyID, Equals, privKey.PublicKey().ID())
+}
+
+func (s *deviceMgrSuite) TestDeviceAssertionsModelAndSerial(c *C) {
+ // nothing in the state
+ s.state.Lock()
+ _, err := devicestate.Model(s.state)
+ s.state.Unlock()
+ c.Check(err, Equals, state.ErrNoState)
+ s.state.Lock()
+ _, err = devicestate.Serial(s.state)
+ s.state.Unlock()
+ c.Check(err, Equals, state.ErrNoState)
+
+ _, err = s.mgr.Model()
+ c.Check(err, Equals, state.ErrNoState)
+ _, err = s.mgr.Serial()
+ c.Check(err, Equals, state.ErrNoState)
+
+ // just brand and model
+ s.state.Lock()
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ s.state.Unlock()
+ _, err = s.mgr.Model()
+ c.Check(err, Equals, state.ErrNoState)
+ _, err = s.mgr.Serial()
+ c.Check(err, Equals, state.ErrNoState)
+
+ // have a model assertion
+ model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "brand-id": "canonical",
+ "model": "pc",
+ "gadget": "pc",
+ "kernel": "kernel",
+ "architecture": "amd64",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ s.state.Lock()
+ err = assertstate.Add(s.state, model)
+ s.state.Unlock()
+ c.Assert(err, IsNil)
+
+ mod, err := s.mgr.Model()
+ c.Assert(err, IsNil)
+ c.Assert(mod.BrandID(), Equals, "canonical")
+
+ s.state.Lock()
+ mod, err = devicestate.Model(s.state)
+ s.state.Unlock()
+ c.Assert(err, IsNil)
+ c.Assert(mod.BrandID(), Equals, "canonical")
+
+ _, err = s.mgr.Serial()
+ c.Check(err, Equals, state.ErrNoState)
+
+ // have a serial as well
+ s.state.Lock()
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ Serial: "8989",
+ })
+ s.state.Unlock()
+ _, err = s.mgr.Model()
+ c.Assert(err, IsNil)
+ _, err = s.mgr.Serial()
+ c.Check(err, Equals, state.ErrNoState)
+
+ // have a serial assertion
+ devKey, _ := assertstest.GenerateKey(752)
+ encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
+ c.Assert(err, IsNil)
+ serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "8989",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": devKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ s.state.Lock()
+ err = assertstate.Add(s.state, serial)
+ s.state.Unlock()
+ c.Assert(err, IsNil)
+
+ _, err = s.mgr.Model()
+ c.Assert(err, IsNil)
+ ser, err := s.mgr.Serial()
+ c.Assert(err, IsNil)
+ c.Check(ser.Serial(), Equals, "8989")
+
+ s.state.Lock()
+ ser, err = devicestate.Serial(s.state)
+ s.state.Unlock()
+ c.Assert(err, IsNil)
+ c.Check(ser.Serial(), Equals, "8989")
+}
+
+func (s *deviceMgrSuite) TestDeviceAssertionsDeviceSessionRequest(c *C) {
+ // nothing there
+ _, _, err := s.mgr.DeviceSessionRequest("NONCE-1")
+ c.Check(err, Equals, state.ErrNoState)
+
+ // setup state as done by device initialisation
+ s.state.Lock()
+ devKey, _ := assertstest.GenerateKey(1024)
+ encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
+ c.Check(err, IsNil)
+ seriala, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "8989",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": devKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, seriala)
+ c.Assert(err, IsNil)
+
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ Serial: "8989",
+ KeyID: devKey.PublicKey().ID(),
+ })
+ s.mgr.KeypairManager().Put(devKey)
+ s.state.Unlock()
+
+ sessReq, serial, err := s.mgr.DeviceSessionRequest("NONCE-1")
+ c.Assert(err, IsNil)
+
+ c.Check(serial.Serial(), Equals, "8989")
+
+ // correctly signed with device key
+ err = asserts.SignatureCheck(sessReq, devKey.PublicKey())
+ c.Check(err, IsNil)
+
+ c.Check(sessReq.BrandID(), Equals, "canonical")
+ c.Check(sessReq.Model(), Equals, "pc")
+ c.Check(sessReq.Serial(), Equals, "8989")
+ c.Check(sessReq.Nonce(), Equals, "NONCE-1")
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlAlreadySeeded(c *C) {
+ release.OnClassic = false
+
+ s.state.Lock()
+ s.state.Set("seeded", true)
+ s.state.Unlock()
+
+ called := false
+ restore := devicestate.MockPopulateStateFromSeed(func(*state.State) ([]*state.TaskSet, error) {
+ called = true
+ return nil, nil
+ })
+ defer restore()
+
+ err := s.mgr.EnsureSeedYaml()
+ c.Assert(err, IsNil)
+ c.Assert(called, Equals, false)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlChangeInFlight(c *C) {
+ release.OnClassic = false
+
+ s.state.Lock()
+ chg := s.state.NewChange("seed", "just for testing")
+ chg.AddTask(s.state.NewTask("test-task", "the change needs a task"))
+ s.state.Unlock()
+
+ called := false
+ restore := devicestate.MockPopulateStateFromSeed(func(*state.State) ([]*state.TaskSet, error) {
+ called = true
+ return nil, nil
+ })
+ defer restore()
+
+ err := s.mgr.EnsureSeedYaml()
+ c.Assert(err, IsNil)
+ c.Assert(called, Equals, false)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlSkippedOnClassic(c *C) {
+ release.OnClassic = true
+
+ called := false
+ restore := devicestate.MockPopulateStateFromSeed(func(*state.State) ([]*state.TaskSet, error) {
+ called = true
+ return nil, nil
+ })
+ defer restore()
+
+ err := s.mgr.EnsureSeedYaml()
+ c.Assert(err, IsNil)
+ c.Assert(called, Equals, false)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlHappy(c *C) {
+ release.OnClassic = false
+
+ restore := devicestate.MockPopulateStateFromSeed(func(*state.State) (ts []*state.TaskSet, err error) {
+ t := s.state.NewTask("test-task", "a random task")
+ ts = append(ts, state.NewTaskSet(t))
+ return ts, nil
+ })
+ defer restore()
+
+ err := s.mgr.EnsureSeedYaml()
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.state.Changes(), HasLen, 1)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlRecover(c *C) {
+ release.OnClassic = false
+
+ restore := devicestate.MockPopulateStateFromSeed(func(*state.State) (ts []*state.TaskSet, err error) {
+ return nil, errors.New("should not be called")
+ })
+ defer restore()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.setupCore(c, "ubuntu-core", `
+name: ubuntu-core
+type: os
+version: ubuntu-core
+`, "")
+
+ // have a model assertion
+ model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "brand-id": "canonical",
+ "model": "pc",
+ "gadget": "pc",
+ "kernel": "kernel",
+ "architecture": "amd64",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, model)
+ c.Assert(err, IsNil)
+
+ // have a serial assertion
+ devKey, _ := assertstest.GenerateKey(752)
+ encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
+ keyID := devKey.PublicKey().ID()
+ c.Assert(err, IsNil)
+ serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{
+ "brand-id": "canonical",
+ "model": "pc",
+ "serial": "8989",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": keyID,
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, serial)
+ c.Assert(err, IsNil)
+
+ // forgotten key id and serial
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ // put key on disk
+ err = s.mgr.KeypairManager().Put(devKey)
+ c.Assert(err, IsNil)
+ // extra unused stuff
+ junk1 := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "junkjunk1")
+ err = ioutil.WriteFile(junk1, nil, 0644)
+ c.Assert(err, IsNil)
+ junk2 := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "junkjunk2")
+ err = ioutil.WriteFile(junk2, nil, 0644)
+ c.Assert(err, IsNil)
+ // double check
+ pat := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "*")
+ onDisk, err := filepath.Glob(pat)
+ c.Assert(err, IsNil)
+ c.Check(onDisk, HasLen, 3)
+
+ s.state.Unlock()
+ err = s.mgr.EnsureSeedYaml()
+ s.state.Lock()
+ c.Assert(err, IsNil)
+
+ c.Check(s.state.Changes(), HasLen, 0)
+
+ var seeded bool
+ err = s.state.Get("seeded", &seeded)
+ c.Assert(err, IsNil)
+ c.Check(seeded, Equals, true)
+
+ device, err := auth.Device(s.state)
+ c.Assert(err, IsNil)
+ c.Check(device, DeepEquals, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ KeyID: keyID,
+ Serial: "8989",
+ })
+ // key is still there
+ _, err = s.mgr.KeypairManager().Get(keyID)
+ c.Assert(err, IsNil)
+ onDisk, err = filepath.Glob(pat)
+ c.Assert(err, IsNil)
+ // junk was removed
+ c.Check(onDisk, HasLen, 1)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkSkippedOnClassic(c *C) {
+ release.OnClassic = true
+
+ err := s.mgr.EnsureBootOk()
+ c.Assert(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) {
+ release.OnClassic = false
+
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ defer partition.ForceBootloader(nil)
+ bootloader.SetBootVars(map[string]string{
+ "snap_mode": "trying",
+ "snap_try_core": "core_1.snap",
+ })
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ siCore1 := &snap.SideInfo{RealName: "core", Revision: snap.R(1)}
+ snapstate.Set(s.state, "core", &snapstate.SnapState{
+ SnapType: "os",
+ Active: true,
+ Sequence: []*snap.SideInfo{siCore1},
+ Current: siCore1.Revision,
+ })
+
+ s.state.Unlock()
+ err := s.mgr.EnsureBootOk()
+ s.state.Lock()
+ c.Assert(err, IsNil)
+
+ m, err := bootloader.GetBootVars("snap_mode")
+ c.Assert(err, IsNil)
+ c.Assert(m, DeepEquals, map[string]string{"snap_mode": ""})
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkUpdateBootRevisionsHappy(c *C) {
+ release.OnClassic = false
+
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ defer partition.ForceBootloader(nil)
+
+ // simulate that we have a new core_2, tried to boot it but that failed
+ bootloader.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_try_core": "core_2.snap",
+ "snap_core": "core_1.snap",
+ })
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ siCore1 := &snap.SideInfo{RealName: "core", Revision: snap.R(1)}
+ siCore2 := &snap.SideInfo{RealName: "core", Revision: snap.R(2)}
+ snapstate.Set(s.state, "core", &snapstate.SnapState{
+ SnapType: "os",
+ Active: true,
+ Sequence: []*snap.SideInfo{siCore1, siCore2},
+ Current: siCore2.Revision,
+ })
+
+ s.state.Unlock()
+ err := s.mgr.EnsureBootOk()
+ s.state.Lock()
+ c.Assert(err, IsNil)
+
+ c.Check(s.state.Changes(), HasLen, 1)
+ c.Check(s.state.Changes()[0].Kind(), Equals, "update-revisions")
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkNotRunAgain(c *C) {
+ release.OnClassic = false
+
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ bootloader.SetBootVars(map[string]string{
+ "snap_mode": "trying",
+ "snap_try_core": "core_1.snap",
+ })
+ bootloader.SetErr = fmt.Errorf("ensure bootloader is not used")
+ partition.ForceBootloader(bootloader)
+ defer partition.ForceBootloader(nil)
+
+ s.mgr.SetBootOkRan(true)
+
+ err := s.mgr.EnsureBootOk()
+ c.Assert(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkError(c *C) {
+ release.OnClassic = false
+
+ s.state.Lock()
+ // seeded
+ s.state.Set("seeded", true)
+ // has serial
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ Serial: "8989",
+ })
+ s.state.Unlock()
+
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ bootloader.GetErr = fmt.Errorf("bootloader err")
+ partition.ForceBootloader(bootloader)
+ defer partition.ForceBootloader(nil)
+
+ s.mgr.SetBootOkRan(false)
+
+ err := s.mgr.Ensure()
+ c.Assert(err, ErrorMatches, "devicemgr: bootloader err")
+}
+
+func (s *deviceMgrSuite) TestCheckGadget(c *C) {
+ release.OnClassic = false
+ s.state.Lock()
+ defer s.state.Unlock()
+ // nothing is setup
+ gadgetInfo := snaptest.MockInfo(c, `type: gadget
+name: gadget`, nil)
+
+ err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install gadget without model assertion`)
+
+ // setup model assertion
+ model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "brand-id": "canonical",
+ "model": "pc",
+ "gadget": "pc",
+ "kernel": "kernel",
+ "architecture": "amd64",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, model)
+ c.Assert(err, IsNil)
+ err = auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ c.Assert(err, IsNil)
+
+ err = devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install gadget "gadget", model assertion requests "pc"`)
+
+ // install pc gadget
+ pcGadgetInfo := snaptest.MockInfo(c, `type: gadget
+name: pc`, nil)
+ err = devicestate.CheckGadgetOrKernel(s.state, pcGadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestCheckKernel(c *C) {
+ release.OnClassic = false
+ s.state.Lock()
+ defer s.state.Unlock()
+ // nothing is setup
+ kernelInfo := snaptest.MockInfo(c, `type: kernel
+name: lnrk`, nil)
+
+ err := devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install kernel without model assertion`)
+
+ // setup model assertion
+ model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "brand-id": "canonical",
+ "model": "pc",
+ "gadget": "pc",
+ "kernel": "krnl",
+ "architecture": "amd64",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, model)
+ c.Assert(err, IsNil)
+ err = auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ c.Assert(err, IsNil)
+
+ err = devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install kernel "lnrk", model assertion requests "krnl"`)
+
+ // install krnl kernel
+ krnlKernelInfo := snaptest.MockInfo(c, `type: kernel
+name: krnl`, nil)
+ err = devicestate.CheckGadgetOrKernel(s.state, krnlKernelInfo, nil, snapstate.Flags{})
+ c.Check(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package devicestate
+
+import (
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+func MockKeyLength(n int) (restore func()) {
+ oldKeyLength := keyLength
+ keyLength = n
+ return func() {
+ keyLength = oldKeyLength
+ }
+}
+
+func MockRequestIDURL(url string) (restore func()) {
+ oldURL := requestIDURL
+ requestIDURL = url
+ return func() {
+ requestIDURL = oldURL
+ }
+}
+
+func MockSerialRequestURL(url string) (restore func()) {
+ oldURL := serialRequestURL
+ serialRequestURL = url
+ return func() {
+ serialRequestURL = oldURL
+ }
+}
+
+func MockRetryInterval(interval time.Duration) (restore func()) {
+ old := retryInterval
+ retryInterval = interval
+ return func() {
+ retryInterval = old
+ }
+}
+
+func (m *DeviceManager) KeypairManager() asserts.KeypairManager {
+ return m.keypairMgr
+}
+
+func MockRepeatRequestSerial(label string) (restore func()) {
+ old := repeatRequestSerial
+ repeatRequestSerial = label
+ return func() {
+ repeatRequestSerial = old
+ }
+}
+
+func (m *DeviceManager) EnsureSeedYaml() error {
+ return m.ensureSeedYaml()
+}
+
+var PopulateStateFromSeedImpl = populateStateFromSeedImpl
+
+func MockPopulateStateFromSeed(f func(*state.State) ([]*state.TaskSet, error)) (restore func()) {
+ old := populateStateFromSeed
+ populateStateFromSeed = f
+ return func() {
+ populateStateFromSeed = old
+ }
+}
+
+func (m *DeviceManager) EnsureBootOk() error {
+ return m.ensureBootOk()
+}
+
+func (m *DeviceManager) SetBootOkRan(b bool) {
+ m.bootOkRan = b
+}
+
+var (
+ ImportAssertionsFromSeed = importAssertionsFromSeed
+ CheckGadgetOrKernel = checkGadgetOrKernel
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package devicestate
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func populateStateFromSeedImpl(st *state.State) ([]*state.TaskSet, error) {
+ // check that the state is empty
+ var seeded bool
+ err := st.Get("seeded", &seeded)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if seeded {
+ return nil, fmt.Errorf("cannot populate state: already seeded")
+ }
+
+ // ack all initial assertions
+ if err := importAssertionsFromSeed(st); err != nil {
+ return nil, err
+ }
+
+ seed, err := snap.ReadSeedYaml(filepath.Join(dirs.SnapSeedDir, "seed.yaml"))
+ if err != nil {
+ return nil, err
+ }
+
+ tsAll := []*state.TaskSet{}
+ for i, sn := range seed.Snaps {
+
+ var flags snapstate.Flags
+ if sn.DevMode {
+ flags.DevMode = true
+ }
+ path := filepath.Join(dirs.SnapSeedDir, "snaps", sn.File)
+
+ var sideInfo snap.SideInfo
+ if sn.Unasserted {
+ sideInfo.RealName = sn.Name
+ } else {
+ si, err := snapasserts.DeriveSideInfo(path, assertstate.DB(st))
+ if err == asserts.ErrNotFound {
+ return nil, fmt.Errorf("cannot find signatures with metadata for snap %q (%q)", sn.Name, path)
+ }
+ if err != nil {
+ return nil, err
+ }
+ sideInfo = *si
+ sideInfo.Private = sn.Private
+ }
+
+ ts, err := snapstate.InstallPath(st, &sideInfo, path, sn.Channel, flags)
+ if i > 0 {
+ ts.WaitAll(tsAll[i-1])
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ tsAll = append(tsAll, ts)
+ }
+ if len(tsAll) == 0 {
+ return nil, nil
+ }
+
+ ts := tsAll[len(tsAll)-1]
+ markSeeded := st.NewTask("mark-seeded", i18n.G("Mark system seeded"))
+ markSeeded.WaitAll(ts)
+ tsAll = append(tsAll, state.NewTaskSet(markSeeded))
+
+ return tsAll, nil
+}
+
+func readAsserts(fn string, batch *assertstate.Batch) ([]*asserts.Ref, error) {
+ f, err := os.Open(fn)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return batch.AddStream(f)
+}
+
+func importAssertionsFromSeed(st *state.State) error {
+ device, err := auth.Device(st)
+ if err != nil {
+ return err
+ }
+
+ // set device,model from the model assertion
+ assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions")
+ dc, err := ioutil.ReadDir(assertSeedDir)
+ if err != nil {
+ return fmt.Errorf("cannot read assert seed dir: %s", err)
+ }
+
+ // FIXME: remove this check once asserts are mandatory
+ if len(dc) == 0 {
+ return nil
+ }
+
+ // collect
+ var modelRef *asserts.Ref
+ batch := assertstate.NewBatch()
+ for _, fi := range dc {
+ fn := filepath.Join(assertSeedDir, fi.Name())
+ refs, err := readAsserts(fn, batch)
+ if err != nil {
+ return fmt.Errorf("cannot read assertions: %s", err)
+ }
+ for _, ref := range refs {
+ if ref.Type == asserts.ModelType {
+ if modelRef != nil && modelRef.Unique() != ref.Unique() {
+ return fmt.Errorf("cannot add more than one model assertion")
+ }
+ modelRef = ref
+ }
+ }
+ }
+ // verify we have one model assertion
+ if modelRef == nil {
+ return fmt.Errorf("need a model assertion")
+ }
+
+ if err := batch.Commit(st); err != nil {
+ return err
+ }
+
+ a, err := modelRef.Resolve(assertstate.DB(st).Find)
+ if err != nil {
+ return fmt.Errorf("internal error: cannot find just added assertion %v: %v", modelRef, err)
+ }
+ modelAssertion := a.(*asserts.Model)
+
+ // set device,model from the model assertion
+ device.Brand = modelAssertion.BrandID()
+ device.Model = modelAssertion.Model()
+ if err := auth.SetDevice(st, device); err != nil {
+ return err
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package devicestate_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type FirstBootTestSuite struct {
+ systemctl *testutil.MockCmd
+ mockUdevAdm *testutil.MockCmd
+
+ storeSigning *assertstest.StoreStack
+ restore func()
+
+ brandPrivKey asserts.PrivateKey
+ brandSigning *assertstest.SigningDB
+
+ overlord *overlord.Overlord
+}
+
+var _ = Suite(&FirstBootTestSuite{})
+
+func (s *FirstBootTestSuite) SetUpTest(c *C) {
+ tempdir := c.MkDir()
+ dirs.SetRootDir(tempdir)
+
+ // mock the world!
+ err := os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "snaps"), 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "assertions"), 0755)
+ c.Assert(err, IsNil)
+
+ err = os.MkdirAll(dirs.SnapServicesDir, 0755)
+ c.Assert(err, IsNil)
+ os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+ s.systemctl = testutil.MockCommand(c, "systemctl", "")
+ s.mockUdevAdm = testutil.MockCommand(c, "udevadm", "")
+
+ err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), nil, 0644)
+ c.Assert(err, IsNil)
+
+ rootPrivKey, _ := assertstest.GenerateKey(1024)
+ storePrivKey, _ := assertstest.GenerateKey(752)
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+ s.restore = sysdb.InjectTrusted(s.storeSigning.Trusted)
+
+ s.brandPrivKey, _ = assertstest.GenerateKey(752)
+ s.brandSigning = assertstest.NewSigningDB("my-brand", s.brandPrivKey)
+
+ ovld, err := overlord.New()
+ c.Assert(err, IsNil)
+ s.overlord = ovld
+}
+
+func (s *FirstBootTestSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+ os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
+ s.systemctl.Restore()
+ s.mockUdevAdm.Restore()
+
+ s.restore()
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedErrorsOnState(c *C) {
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ st.Set("seeded", true)
+
+ _, err := devicestate.PopulateStateFromSeedImpl(st)
+ c.Assert(err, ErrorMatches, "cannot populate state: already seeded")
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedHappy(c *C) {
+ // put a firstboot snap into the SnapBlobDir
+ snapYaml := `name: foo
+version: 1.0`
+ mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
+ targetSnapFile := filepath.Join(dirs.SnapSeedDir, "snaps", filepath.Base(mockSnapFile))
+ err := os.Rename(mockSnapFile, targetSnapFile)
+ c.Assert(err, IsNil)
+
+ // put a firstboot local snap into the SnapBlobDir
+ snapYaml = `name: local
+version: 1.0`
+ mockSnapFile = snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
+ targetSnapFile2 := filepath.Join(dirs.SnapSeedDir, "snaps", filepath.Base(mockSnapFile))
+ err = os.Rename(mockSnapFile, targetSnapFile2)
+ c.Assert(err, IsNil)
+
+ devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
+ "account-id": "developerid",
+ }, "")
+ devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
+ err = ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
+ c.Assert(err, IsNil)
+
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "snapidsnapid",
+ "publisher-id": "developerid",
+ "snap-name": "foo",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ declFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-declaration")
+ err = ioutil.WriteFile(declFn, asserts.Encode(snapDecl), 0644)
+ c.Assert(err, IsNil)
+
+ sha3_384, size, err := asserts.SnapFileSHA3_384(targetSnapFile)
+ c.Assert(err, IsNil)
+
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": sha3_384,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-id": "snapidsnapid",
+ "developer-id": "developerid",
+ "snap-revision": "128",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ revFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-revision")
+ err = ioutil.WriteFile(revFn, asserts.Encode(snapRev), 0644)
+ c.Assert(err, IsNil)
+
+ // add a model assertion and its chain
+ assertsChain := s.makeModelAssertionChain(c)
+ for i, as := range assertsChain {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ }
+
+ // create a seed.yaml
+ content := []byte(fmt.Sprintf(`
+snaps:
+ - name: foo
+ file: %s
+ devmode: true
+ - name: local
+ unasserted: true
+ file: %s
+`, filepath.Base(targetSnapFile), filepath.Base(targetSnapFile2)))
+ err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ // run the firstboot stuff
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ tsAll, err := devicestate.PopulateStateFromSeedImpl(st)
+ c.Assert(err, IsNil)
+
+ // the last task of the last taskset must be mark-seeded
+ markSeededTask := tsAll[len(tsAll)-1].Tasks()[0]
+ c.Check(markSeededTask.Kind(), Equals, "mark-seeded")
+ // and the markSeededTask must wait for the other tasks
+ prevTasks := tsAll[len(tsAll)-2].Tasks()
+ otherTask := prevTasks[len(prevTasks)-1]
+ c.Check(markSeededTask.WaitTasks(), testutil.Contains, otherTask)
+
+ // now run the change and check the result
+ chg := st.NewChange("run-it", "run the populate from seed changes")
+ for _, ts := range tsAll {
+ chg.AddAll(ts)
+ }
+ c.Assert(st.Changes(), HasLen, 1)
+
+ st.Unlock()
+ s.overlord.Settle()
+ st.Lock()
+ c.Assert(chg.Err(), IsNil)
+
+ // and check the snap got correctly installed
+ c.Check(osutil.FileExists(filepath.Join(dirs.SnapMountDir, "foo", "128", "meta", "snap.yaml")), Equals, true)
+
+ c.Check(osutil.FileExists(filepath.Join(dirs.SnapMountDir, "local", "x1", "meta", "snap.yaml")), Equals, true)
+
+ // verify
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ state, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ state.Lock()
+ defer state.Unlock()
+ // check foo
+ info, err := snapstate.CurrentInfo(state, "foo")
+ c.Assert(err, IsNil)
+ c.Assert(info.SnapID, Equals, "snapidsnapid")
+ c.Assert(info.Revision, Equals, snap.R(128))
+ pubAcct, err := assertstate.Publisher(st, info.SnapID)
+ c.Assert(err, IsNil)
+ c.Check(pubAcct.AccountID(), Equals, "developerid")
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(state, "foo", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.DevMode, Equals, true)
+
+ // check local
+ info, err = snapstate.CurrentInfo(state, "local")
+ c.Assert(err, IsNil)
+ c.Assert(info.SnapID, Equals, "")
+ c.Assert(info.Revision, Equals, snap.R("x1"))
+
+ // and ensure state is now considered seeded
+ var seeded bool
+ err = state.Get("seeded", &seeded)
+ c.Assert(err, IsNil)
+ c.Check(seeded, Equals, true)
+}
+
+func writeAssertionsToFile(fn string, assertions []asserts.Assertion) {
+ multifn := filepath.Join(dirs.SnapSeedDir, "assertions", fn)
+ f, err := os.Create(multifn)
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ enc := asserts.NewEncoder(f)
+ for _, a := range assertions {
+ err := enc.Encode(a)
+ if err != nil {
+ panic(err)
+ }
+ }
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedHappyMultiAssertsFiles(c *C) {
+ // put a firstboot snap into the SnapBlobDir
+ snapYaml := `name: foo
+version: 1.0`
+ mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
+ fooSnapFile := filepath.Join(dirs.SnapSeedDir, "snaps", filepath.Base(mockSnapFile))
+ err := os.Rename(mockSnapFile, fooSnapFile)
+ c.Assert(err, IsNil)
+
+ // put a 2nd firstboot snap into the SnapBlobDir
+ snapYaml = `name: bar
+version: 1.0`
+ mockSnapFile = snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
+ barSnapFile := filepath.Join(dirs.SnapSeedDir, "snaps", filepath.Base(mockSnapFile))
+ err = os.Rename(mockSnapFile, barSnapFile)
+ c.Assert(err, IsNil)
+
+ devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
+ "account-id": "developerid",
+ }, "")
+
+ snapDeclFoo, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "foosnapidsnapid",
+ "publisher-id": "developerid",
+ "snap-name": "foo",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ sha3_384, size, err := asserts.SnapFileSHA3_384(fooSnapFile)
+ c.Assert(err, IsNil)
+
+ snapRevFoo, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": sha3_384,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-id": "foosnapidsnapid",
+ "developer-id": "developerid",
+ "snap-revision": "128",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ writeAssertionsToFile("foo.asserts", []asserts.Assertion{devAcct, snapRevFoo, snapDeclFoo})
+
+ snapDeclBar, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "barsnapidsnapid",
+ "publisher-id": "developerid",
+ "snap-name": "bar",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ sha3_384, size, err = asserts.SnapFileSHA3_384(barSnapFile)
+ c.Assert(err, IsNil)
+
+ snapRevBar, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": sha3_384,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-id": "barsnapidsnapid",
+ "developer-id": "developerid",
+ "snap-revision": "65",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ writeAssertionsToFile("bar.asserts", []asserts.Assertion{devAcct, snapDeclBar, snapRevBar})
+
+ // add a model assertion and its chain
+ assertsChain := s.makeModelAssertionChain(c)
+ writeAssertionsToFile("model.asserts", assertsChain)
+
+ // create a seed.yaml
+ content := []byte(fmt.Sprintf(`
+snaps:
+ - name: foo
+ file: %s
+ - name: bar
+ file: %s
+`, filepath.Base(fooSnapFile), filepath.Base(barSnapFile)))
+ err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ // run the firstboot stuff
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ tsAll, err := devicestate.PopulateStateFromSeedImpl(st)
+ c.Assert(err, IsNil)
+ chg := st.NewChange("run-it", "run the populate from seed changes")
+ for _, ts := range tsAll {
+ chg.AddAll(ts)
+ }
+ c.Assert(st.Changes(), HasLen, 1)
+
+ st.Unlock()
+ s.overlord.Settle()
+ st.Lock()
+ c.Assert(chg.Err(), IsNil)
+
+ // and check the snap got correctly installed
+ c.Check(osutil.FileExists(filepath.Join(dirs.SnapMountDir, "foo", "128", "meta", "snap.yaml")), Equals, true)
+
+ // and check the snap got correctly installed
+ c.Check(osutil.FileExists(filepath.Join(dirs.SnapMountDir, "bar", "65", "meta", "snap.yaml")), Equals, true)
+
+ // verify
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ state, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ state.Lock()
+ defer state.Unlock()
+ // check foo
+ info, err := snapstate.CurrentInfo(state, "foo")
+ c.Assert(err, IsNil)
+ c.Check(info.SnapID, Equals, "foosnapidsnapid")
+ c.Check(info.Revision, Equals, snap.R(128))
+ pubAcct, err := assertstate.Publisher(st, info.SnapID)
+ c.Assert(err, IsNil)
+ c.Check(pubAcct.AccountID(), Equals, "developerid")
+
+ // check bar
+ info, err = snapstate.CurrentInfo(state, "bar")
+ c.Assert(err, IsNil)
+ c.Check(info.SnapID, Equals, "barsnapidsnapid")
+ c.Check(info.Revision, Equals, snap.R(65))
+ pubAcct, err = assertstate.Publisher(st, info.SnapID)
+ c.Assert(err, IsNil)
+ c.Check(pubAcct.AccountID(), Equals, "developerid")
+}
+
+func (s *FirstBootTestSuite) makeModelAssertion(c *C, modelStr string) *asserts.Model {
+ headers := map[string]interface{}{
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": modelStr,
+ "architecture": "amd64",
+ "store": "canonical",
+ "gadget": "pc",
+ "kernel": "pc-kernel",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ model, err := s.brandSigning.Sign(asserts.ModelType, headers, nil, "")
+ c.Assert(err, IsNil)
+ return model.(*asserts.Model)
+}
+
+func (s *FirstBootTestSuite) makeModelAssertionChain(c *C) []asserts.Assertion {
+ assertChain := []asserts.Assertion{}
+
+ brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
+ "account-id": "my-brand",
+ "verification": "certified",
+ }, "")
+ assertChain = append(assertChain, brandAcct)
+
+ brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, s.brandPrivKey.PublicKey(), "")
+ assertChain = append(assertChain, brandAccKey)
+
+ model := s.makeModelAssertion(c, "my-model")
+ assertChain = append(assertChain, model)
+
+ storeAccountKey := s.storeSigning.StoreAccountKey("")
+ assertChain = append(assertChain, storeAccountKey)
+ return assertChain
+}
+
+func (s *FirstBootTestSuite) TestImportAssertionsFromSeedHappy(c *C) {
+ ovld, err := overlord.New()
+ c.Assert(err, IsNil)
+ st := ovld.State()
+
+ // add a bunch of assert files
+ assertsChain := s.makeModelAssertionChain(c)
+ for i, as := range assertsChain {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ }
+
+ // import them
+ st.Lock()
+ defer st.Unlock()
+
+ err = devicestate.ImportAssertionsFromSeed(st)
+ c.Assert(err, IsNil)
+
+ // verify that the model was added
+ db := assertstate.DB(st)
+ as, err := db.Find(asserts.ModelType, map[string]string{
+ "series": "16",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ })
+ c.Assert(err, IsNil)
+ _, ok := as.(*asserts.Model)
+ c.Check(ok, Equals, true)
+
+ ds, err := auth.Device(st)
+ c.Assert(err, IsNil)
+ c.Check(ds.Brand, Equals, "my-brand")
+ c.Check(ds.Model, Equals, "my-model")
+}
+
+func (s *FirstBootTestSuite) TestImportAssertionsFromSeedMissingSig(c *C) {
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // write out only the model assertion
+ assertsChain := s.makeModelAssertionChain(c)
+ for _, as := range assertsChain {
+ if as.Type() == asserts.ModelType {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ break
+ }
+ }
+
+ // try import and verify that its rejects because other assertions are
+ // missing
+ err := devicestate.ImportAssertionsFromSeed(st)
+ c.Assert(err, ErrorMatches, "cannot find account-key .*: assertion not found")
+}
+
+func (s *FirstBootTestSuite) TestImportAssertionsFromSeedTwoModelAsserts(c *C) {
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // write out two model assertions
+ model := s.makeModelAssertion(c, "my-model")
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
+ err := ioutil.WriteFile(fn, asserts.Encode(model), 0644)
+ c.Assert(err, IsNil)
+
+ model2 := s.makeModelAssertion(c, "my-second-model")
+ fn = filepath.Join(dirs.SnapSeedDir, "assertions", "model2")
+ err = ioutil.WriteFile(fn, asserts.Encode(model2), 0644)
+ c.Assert(err, IsNil)
+
+ // try import and verify that its rejects because other assertions are
+ // missing
+ err = devicestate.ImportAssertionsFromSeed(st)
+ c.Assert(err, ErrorMatches, "cannot add more than one model assertion")
+}
+
+func (s *FirstBootTestSuite) TestImportAssertionsFromSeedNoModelAsserts(c *C) {
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ assertsChain := s.makeModelAssertionChain(c)
+ for _, as := range assertsChain {
+ if as.Type() != asserts.ModelType {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ break
+ }
+ }
+
+ // try import and verify that its rejects because other assertions are
+ // missing
+ err := devicestate.ImportAssertionsFromSeed(st)
+ c.Assert(err, ErrorMatches, "need a model assertion")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package overlord
+
+import (
+ "time"
+
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/store"
+)
+
+// MockEnsureInterval sets the overlord ensure interval for tests.
+func MockEnsureInterval(d time.Duration) (restore func()) {
+ old := ensureInterval
+ ensureInterval = d
+ return func() { ensureInterval = old }
+}
+
+// MockPruneInterval sets the overlord prune interval for tests.
+func MockPruneInterval(prunei, prunew, abortw time.Duration) (restore func()) {
+ oldPruneInterval := pruneInterval
+ oldPruneWait := pruneWait
+ oldAbortWait := abortWait
+ pruneInterval = prunei
+ pruneWait = prunew
+ abortWait = abortw
+ return func() {
+ pruneInterval = oldPruneInterval
+ pruneWait = oldPruneWait
+ abortWait = oldAbortWait
+ }
+}
+
+// MockEnsureNext sets o.ensureNext for tests.
+func MockEnsureNext(o *Overlord, t time.Time) {
+ o.ensureNext = t
+}
+
+// Engine exposes the state engine in an Overlord for tests.
+func (o *Overlord) Engine() *StateEngine {
+ return o.stateEng
+}
+
+// MockStoreNew mocks store.New as called by overlord.New.
+func MockStoreNew(new func(*store.Config, auth.AuthContext) *store.Store) (restore func()) {
+ storeNew = new
+ return func() {
+ storeNew = store.New
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hookstate
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "sync/atomic"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Context represents the context under which a given hook is running.
+type Context struct {
+ task *state.Task
+ setup *HookSetup
+ id string
+ handler Handler
+
+ cache map[interface{}]interface{}
+ onDone []func() error
+
+ mutex sync.Mutex
+ mutexChecker int32
+}
+
+// NewContext returns a new Context.
+func NewContext(task *state.Task, setup *HookSetup, handler Handler) (*Context, error) {
+ // Generate a secure, random ID for this context
+ idBytes := make([]byte, 32)
+ _, err := rand.Read(idBytes)
+ if err != nil {
+ return nil, fmt.Errorf("cannot generate context ID: %s", err)
+ }
+
+ return &Context{
+ task: task,
+ setup: setup,
+ id: base64.URLEncoding.EncodeToString(idBytes),
+ handler: handler,
+ cache: make(map[interface{}]interface{}),
+ }, nil
+}
+
+// SnapName returns the name of the snap containing the hook.
+func (c *Context) SnapName() string {
+ return c.setup.Snap
+}
+
+// SnapRevision returns the revision of the snap containing the hook.
+func (c *Context) SnapRevision() snap.Revision {
+ return c.setup.Revision
+}
+
+// HookName returns the name of the hook in this context.
+func (c *Context) HookName() string {
+ return c.setup.Hook
+}
+
+// ID returns the ID of the context.
+func (c *Context) ID() string {
+ return c.id
+}
+
+// Handler returns the handler for this context
+func (c *Context) Handler() Handler {
+ return c.handler
+}
+
+// Lock acquires the lock for this context (required for Set/Get, Cache/Cached),
+// and OnDone/Done).
+func (c *Context) Lock() {
+ c.mutex.Lock()
+ c.task.State().Lock()
+ atomic.AddInt32(&c.mutexChecker, 1)
+}
+
+// Unlock releases the lock for this context.
+func (c *Context) Unlock() {
+ atomic.AddInt32(&c.mutexChecker, -1)
+ c.task.State().Unlock()
+ c.mutex.Unlock()
+}
+
+func (c *Context) reading() {
+ if atomic.LoadInt32(&c.mutexChecker) != 1 {
+ panic("internal error: accessing context without lock")
+ }
+}
+
+func (c *Context) writing() {
+ if atomic.LoadInt32(&c.mutexChecker) != 1 {
+ panic("internal error: accessing context without lock")
+ }
+}
+
+// Set associates value with key. The provided value must properly marshal and
+// unmarshal with encoding/json. Note that the context needs to be locked and
+// unlocked by the caller.
+func (c *Context) Set(key string, value interface{}) {
+ c.writing()
+
+ var data map[string]*json.RawMessage
+ if err := c.task.Get("hook-context", &data); err != nil && err != state.ErrNoState {
+ panic(fmt.Sprintf("internal error: cannot unmarshal context: %v", err))
+ }
+ if data == nil {
+ data = make(map[string]*json.RawMessage)
+ }
+
+ marshalledValue, err := json.Marshal(value)
+ if err != nil {
+ panic(fmt.Sprintf("internal error: cannot marshal context value for %q: %s", key, err))
+ }
+ raw := json.RawMessage(marshalledValue)
+ data[key] = &raw
+
+ c.task.Set("hook-context", data)
+}
+
+// Get unmarshals the stored value associated with the provided key into the
+// value parameter. Note that the context needs to be locked/unlocked by the
+// caller.
+func (c *Context) Get(key string, value interface{}) error {
+ c.reading()
+
+ var data map[string]*json.RawMessage
+ if err := c.task.Get("hook-context", &data); err != nil {
+ return err
+ }
+
+ raw, ok := data[key]
+ if !ok {
+ return state.ErrNoState
+ }
+
+ err := json.Unmarshal([]byte(*raw), &value)
+ if err != nil {
+ return fmt.Errorf("cannot unmarshal context value for %q: %s", key, err)
+ }
+
+ return nil
+}
+
+// State returns the state contained within the context
+func (c *Context) State() *state.State {
+ return c.task.State()
+}
+
+// Cached returns the cached value associated with the provided key. It returns
+// nil if there is no entry for key. Note that the context needs to be locked
+// and unlocked by the caller.
+func (c *Context) Cached(key interface{}) interface{} {
+ c.reading()
+
+ return c.cache[key]
+}
+
+// Cache associates value with key. The cached value is not persisted. Note that
+// the context needs to be locked/unlocked by the caller.
+func (c *Context) Cache(key, value interface{}) {
+ c.writing()
+
+ c.cache[key] = value
+}
+
+// OnDone requests the provided function to be run once the context knows it's
+// complete. This can be called multiple times; each function will be called in
+// the order in which they were added. Note that the context needs to be locked
+// and unlocked by the caller.
+func (c *Context) OnDone(f func() error) {
+ c.writing()
+
+ c.onDone = append(c.onDone, f)
+}
+
+// Done is called to notify the context that its hook has exited successfully.
+// It will call all of the functions added in OnDone (even if one of them
+// returns an error) and will return the first error encountered. Note that the
+// context needs to be locked/unlocked by the caller.
+func (c *Context) Done() error {
+ c.reading()
+
+ var firstErr error
+ for _, f := range c.onDone {
+ if err := f(); err != nil && firstErr == nil {
+ firstErr = err
+ }
+ }
+
+ return firstErr
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hookstate
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type contextSuite struct {
+ context *Context
+ task *state.Task
+ setup *HookSetup
+}
+
+var _ = Suite(&contextSuite{})
+
+func (s *contextSuite) SetUpTest(c *C) {
+ state := state.New(nil)
+ state.Lock()
+ defer state.Unlock()
+
+ s.task = state.NewTask("test-task", "my test task")
+ s.setup = &HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+ var err error
+ s.context, err = NewContext(s.task, s.setup, nil)
+ c.Check(err, IsNil)
+}
+
+func (s *contextSuite) TestHookSetup(c *C) {
+ c.Check(s.context.HookName(), Equals, "test-hook")
+ c.Check(s.context.SnapName(), Equals, "test-snap")
+}
+
+func (s *contextSuite) TestSetAndGet(c *C) {
+ s.context.Lock()
+ defer s.context.Unlock()
+
+ var output string
+ c.Check(s.context.Get("foo", &output), NotNil)
+
+ s.context.Set("foo", "bar")
+ c.Check(s.context.Get("foo", &output), IsNil, Commentf("Expected context to contain 'foo'"))
+ c.Check(output, Equals, "bar")
+
+ // Test another non-existing key, but after the context data was created.
+ c.Check(s.context.Get("baz", &output), NotNil)
+}
+
+func (s *contextSuite) TestSetPersistence(c *C) {
+ s.context.Lock()
+ s.context.Set("foo", "bar")
+ s.context.Unlock()
+
+ // Verify that "foo" is still "bar" within another context of the same hook
+ // on the same task.
+ anotherContext := &Context{task: s.task, setup: s.setup}
+ anotherContext.Lock()
+ defer anotherContext.Unlock()
+
+ var output string
+ c.Check(anotherContext.Get("foo", &output), IsNil, Commentf("Expected new context to also contain 'foo'"))
+ c.Check(output, Equals, "bar")
+}
+
+func (s *contextSuite) TestSetUnmarshalable(c *C) {
+ s.context.Lock()
+ defer s.context.Unlock()
+
+ defer func() {
+ c.Check(recover(), Matches, ".*cannot marshal context value.*", Commentf("Expected panic when attempting install"))
+ }()
+
+ s.context.Set("foo", func() {})
+}
+
+func (s *contextSuite) TestGetIsolatedFromTask(c *C) {
+ // Set data in the task itself
+ s.task.State().Lock()
+ s.task.Set("foo", "bar")
+ s.task.State().Unlock()
+
+ s.context.Lock()
+ defer s.context.Unlock()
+
+ // Verify that "foo" is not set when asking for data from the hook context
+ var output string
+ c.Check(s.context.Get("foo", &output), NotNil, Commentf("Expected context data to be isolated from task"))
+}
+
+func (s *contextSuite) TestCache(c *C) {
+ s.context.Lock()
+ defer s.context.Unlock()
+
+ c.Check(s.context.Cached("foo"), IsNil)
+
+ s.context.Cache("foo", "bar")
+ c.Check(s.context.Cached("foo"), Equals, "bar")
+
+ // Test another non-existing key, but after the context cache was created.
+ c.Check(s.context.Cached("baz"), IsNil)
+}
+
+func (s *contextSuite) TestDone(c *C) {
+ s.context.Lock()
+ defer s.context.Unlock()
+
+ called := false
+ s.context.OnDone(func() error {
+ called = true
+ return nil
+ })
+
+ s.context.Done()
+ c.Check(called, Equals, true, Commentf("Expected finalizer to be called"))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package ctlcmd contains the various snapctl subcommands.
+package ctlcmd
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/hookstate"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type baseCommand struct {
+ stdout io.Writer
+ stderr io.Writer
+ c *hookstate.Context
+}
+
+func (c *baseCommand) setStdout(w io.Writer) {
+ c.stdout = w
+}
+
+func (c *baseCommand) printf(format string, a ...interface{}) {
+ if c.stdout != nil {
+ c.stdout.Write([]byte(fmt.Sprintf(format, a...)))
+ }
+}
+
+func (c *baseCommand) setStderr(w io.Writer) {
+ c.stderr = w
+}
+
+func (c *baseCommand) errorf(format string, a ...interface{}) {
+ if c.stderr != nil {
+ c.stderr.Write([]byte(fmt.Sprintf(format, a...)))
+ }
+}
+
+func (c *baseCommand) setContext(context *hookstate.Context) {
+ c.c = context
+}
+
+func (c *baseCommand) context() *hookstate.Context {
+ return c.c
+}
+
+type command interface {
+ setStdout(w io.Writer)
+ setStderr(w io.Writer)
+
+ setContext(context *hookstate.Context)
+ context() *hookstate.Context
+
+ Execute(args []string) error
+}
+
+type commandInfo struct {
+ shortHelp string
+ longHelp string
+ generator func() command
+}
+
+var commands = make(map[string]*commandInfo)
+
+func addCommand(name, shortHelp, longHelp string, generator func() command) {
+ commands[name] = &commandInfo{
+ shortHelp: shortHelp,
+ longHelp: longHelp,
+ generator: generator,
+ }
+}
+
+// Run runs the requested command.
+func Run(context *hookstate.Context, args []string) (stdout, stderr []byte, err error) {
+ parser := flags.NewParser(nil, flags.PassDoubleDash|flags.HelpFlag)
+
+ // Create stdout/stderr buffers, and make sure commands use them.
+ var stdoutBuffer bytes.Buffer
+ var stderrBuffer bytes.Buffer
+ for name, cmdInfo := range commands {
+ cmd := cmdInfo.generator()
+ cmd.setStdout(&stdoutBuffer)
+ cmd.setStderr(&stderrBuffer)
+ cmd.setContext(context)
+
+ _, err = parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, cmd)
+ if err != nil {
+ logger.Panicf("cannot add command %q: %s", name, err)
+ }
+ }
+
+ _, err = parser.ParseArgs(args)
+ return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ctlcmd_test
+
+import (
+ "testing"
+
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type ctlcmdSuite struct {
+ mockContext *hookstate.Context
+}
+
+var _ = Suite(&ctlcmdSuite{})
+
+func (s *ctlcmdSuite) SetUpTest(c *C) {
+ handler := hooktest.NewMockHandler()
+
+ state := state.New(nil)
+ state.Lock()
+ defer state.Unlock()
+
+ task := state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+
+ var err error
+ s.mockContext, err = hookstate.NewContext(task, setup, handler)
+ c.Assert(err, IsNil)
+}
+
+func (s *ctlcmdSuite) TestNonExistingCommand(c *C) {
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"foo"})
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+ c.Check(err, ErrorMatches, ".*[Uu]nknown command.*")
+}
+
+func (s *ctlcmdSuite) TestCommandOutput(c *C) {
+ mockCommand := ctlcmd.AddMockCommand("mock")
+ defer ctlcmd.RemoveCommand("mock")
+
+ mockCommand.FakeStdout = "test stdout"
+ mockCommand.FakeStderr = "test stderr"
+
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"mock", "foo"})
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "test stdout")
+ c.Check(string(stderr), Equals, "test stderr")
+ c.Check(mockCommand.Args, DeepEquals, []string{"foo"})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ctlcmd
+
+import "fmt"
+
+func AddMockCommand(name string) *MockCommand {
+ mockCommand := NewMockCommand()
+ addCommand(name, "", "", func() command { return mockCommand })
+ return mockCommand
+}
+
+func RemoveCommand(name string) {
+ delete(commands, name)
+}
+
+type MockCommand struct {
+ baseCommand
+
+ ExecuteError bool
+ FakeStdout string
+ FakeStderr string
+
+ Args []string
+}
+
+func NewMockCommand() *MockCommand {
+ return &MockCommand{
+ ExecuteError: false,
+ }
+}
+
+func (c *MockCommand) Execute(args []string) error {
+ c.Args = args
+
+ if c.FakeStdout != "" {
+ c.printf(c.FakeStdout)
+ }
+
+ if c.FakeStderr != "" {
+ c.errorf(c.FakeStderr)
+ }
+
+ if c.ExecuteError {
+ return fmt.Errorf("failed at user request")
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ctlcmd
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/overlord/configstate"
+)
+
+type getCommand struct {
+ baseCommand
+
+ Positional struct {
+ Keys []string `positional-arg-name:"<keys>" description:"option keys" required:"yes"`
+ } `positional-args:"yes" required:"yes"`
+
+ Document bool `short:"d" description:"always return document, even with single key"`
+ Typed bool `short:"t" description:"strict typing with nulls and quoted strings"`
+}
+
+var shortGetHelp = i18n.G("Prints configuration options")
+var longGetHelp = i18n.G(`
+The get command prints configuration options for the current snap.
+
+ $ snapctl get username
+ frank
+
+If multiple option names are provided, a document is returned:
+
+ $ snapctl get username password
+ {
+ "username": "frank",
+ "password": "..."
+ }
+
+Nested values may be retrieved via a dotted path:
+
+ $ snapctl get author.name
+ frank
+`)
+
+func init() {
+ addCommand("get", shortGetHelp, longGetHelp, func() command { return &getCommand{} })
+}
+
+func (c *getCommand) Execute(args []string) error {
+ context := c.context()
+ if context == nil {
+ return fmt.Errorf("cannot get without a context")
+ }
+
+ if c.Typed && c.Document {
+ return fmt.Errorf("cannot use -d and -t together")
+ }
+
+ patch := make(map[string]interface{})
+ context.Lock()
+ transaction := configstate.ContextTransaction(context)
+ context.Unlock()
+
+ for _, key := range c.Positional.Keys {
+ var value interface{}
+ err := transaction.Get(c.context().SnapName(), key, &value)
+ if err == nil {
+ patch[key] = value
+ } else if configstate.IsNoOption(err) {
+ if !c.Typed {
+ value = ""
+ }
+ } else {
+ return err
+ }
+ }
+
+ var confToPrint interface{} = patch
+ if !c.Document && len(c.Positional.Keys) == 1 {
+ confToPrint = patch[c.Positional.Keys[0]]
+ }
+
+ if c.Typed && confToPrint == nil {
+ c.printf("null\n")
+ return nil
+ }
+
+ if s, ok := confToPrint.(string); ok && !c.Typed {
+ c.printf("%s\n", s)
+ return nil
+ }
+
+ var bytes []byte
+ if confToPrint != nil {
+ var err error
+ bytes, err = json.MarshalIndent(confToPrint, "", "\t")
+ if err != nil {
+ return err
+ }
+ }
+
+ c.printf("%s\n", string(bytes))
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ctlcmd_test
+
+import (
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+
+ . "gopkg.in/check.v1"
+ "strings"
+)
+
+type getSuite struct {
+ mockContext *hookstate.Context
+ mockHandler *hooktest.MockHandler
+}
+
+var _ = Suite(&getSuite{})
+
+func (s *getSuite) SetUpTest(c *C) {
+ s.mockHandler = hooktest.NewMockHandler()
+
+ state := state.New(nil)
+ state.Lock()
+ defer state.Unlock()
+
+ task := state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+
+ var err error
+ s.mockContext, err = hookstate.NewContext(task, setup, s.mockHandler)
+ c.Assert(err, IsNil)
+
+ // Initialize configuration
+ transaction := configstate.NewTransaction(state)
+ transaction.Set("test-snap", "initial-key", "initial-value")
+ transaction.Commit()
+}
+
+var getTests = []struct {
+ args, stdout, error string
+}{{
+ args: "get --foo",
+ error: ".*unknown flag.*foo.*",
+}, {
+ args: "get test-key1",
+ stdout: "test-value1\n",
+}, {
+ args: "get test-key2",
+ stdout: "2\n",
+}, {
+ args: "get missing-key",
+ stdout: "\n",
+}, {
+ args: "get -t test-key1",
+ stdout: "\"test-value1\"\n",
+}, {
+ args: "get -t test-key2",
+ stdout: "2\n",
+}, {
+ args: "get -t missing-key",
+ stdout: "null\n",
+}, {
+ args: "get -d test-key1",
+ stdout: "{\n\t\"test-key1\": \"test-value1\"\n}\n",
+}, {
+ args: "get test-key1 test-key2",
+ stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n",
+}}
+
+func (s *getSuite) TestGetTests(c *C) {
+ for _, test := range getTests {
+ c.Logf("Test: %s", test.args)
+
+ mockHandler := hooktest.NewMockHandler()
+
+ state := state.New(nil)
+ state.Lock()
+
+ task := state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+
+ var err error
+ mockContext, err := hookstate.NewContext(task, setup, mockHandler)
+ c.Check(err, IsNil)
+
+ // Initialize configuration
+ t := configstate.NewTransaction(state)
+ t.Set("test-snap", "test-key1", "test-value1")
+ t.Set("test-snap", "test-key2", 2)
+ t.Commit()
+
+ state.Unlock()
+
+ stdout, stderr, err := ctlcmd.Run(mockContext, strings.Fields(test.args))
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ } else {
+ c.Check(err, IsNil)
+ c.Check(string(stderr), Equals, "")
+ c.Check(string(stdout), Equals, test.stdout)
+ }
+ }
+}
+
+func (s *getSuite) TestCommandWithoutContext(c *C) {
+ _, _, err := ctlcmd.Run(nil, []string{"get", "foo"})
+ c.Check(err, ErrorMatches, ".*cannot get without a context.*")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ctlcmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/overlord/configstate"
+)
+
+type setCommand struct {
+ baseCommand
+
+ Positional struct {
+ ConfValues []string `positional-arg-name:"key=value" required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+var shortSetHelp = i18n.G("Changes configuration options")
+var longSetHelp = i18n.G(`
+The set command changes the provided configuration options as requested.
+
+ $ snapctl set username=frank password=$PASSWORD
+
+All configuration changes are persisted at once, and only after the hook
+returns successfully.
+
+Nested values may be modified via a dotted path:
+
+ $ snapctl set author.name=frank
+`)
+
+func init() {
+ addCommand("set", shortSetHelp, longSetHelp, func() command { return &setCommand{} })
+}
+
+func (s *setCommand) Execute(args []string) error {
+ context := s.context()
+ if context == nil {
+ return fmt.Errorf("cannot set without a context")
+ }
+
+ context.Lock()
+ transaction := configstate.ContextTransaction(context)
+ context.Unlock()
+
+ for _, patchValue := range s.Positional.ConfValues {
+ parts := strings.SplitN(patchValue, "=", 2)
+ if len(parts) != 2 {
+ return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), patchValue)
+ }
+ key := parts[0]
+ var value interface{}
+ err := json.Unmarshal([]byte(parts[1]), &value)
+ if err != nil {
+ // Not valid JSON-- just save the string as-is.
+ value = parts[1]
+ }
+
+ transaction.Set(s.context().SnapName(), key, value)
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ctlcmd_test
+
+import (
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+
+ . "gopkg.in/check.v1"
+)
+
+type setSuite struct {
+ mockContext *hookstate.Context
+ mockHandler *hooktest.MockHandler
+}
+
+var _ = Suite(&setSuite{})
+
+func (s *setSuite) SetUpTest(c *C) {
+ s.mockHandler = hooktest.NewMockHandler()
+
+ state := state.New(nil)
+ state.Lock()
+ defer state.Unlock()
+
+ task := state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+
+ var err error
+ s.mockContext, err = hookstate.NewContext(task, setup, s.mockHandler)
+ c.Assert(err, IsNil)
+}
+
+func (s *setSuite) TestInvalidArguments(c *C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "foo", "bar"})
+ c.Check(err, ErrorMatches, ".*invalid parameter.*want key=value.*")
+}
+
+func (s *setSuite) TestCommand(c *C) {
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "foo=bar", "baz=qux"})
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+
+ // Verify that the previous set doesn't modify the global state
+ s.mockContext.State().Lock()
+ transaction := configstate.NewTransaction(s.mockContext.State())
+ s.mockContext.State().Unlock()
+ var value string
+ c.Check(transaction.Get("test-snap", "foo", &value), ErrorMatches, ".*snap.*has no.*configuration.*")
+ c.Check(transaction.Get("test-snap", "baz", &value), ErrorMatches, ".*snap.*has no.*configuration.*")
+
+ // Notify the context that we're done. This should save the config.
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+ c.Check(s.mockContext.Done(), IsNil)
+
+ // Verify that the global config has been updated.
+ transaction = configstate.NewTransaction(s.mockContext.State())
+ c.Check(transaction.Get("test-snap", "foo", &value), IsNil)
+ c.Check(value, Equals, "bar")
+ c.Check(transaction.Get("test-snap", "baz", &value), IsNil)
+ c.Check(value, Equals, "qux")
+}
+
+func (s *setSuite) TestCommandSavesDeltasOnly(c *C) {
+ // Setup an initial configuration
+ s.mockContext.State().Lock()
+ transaction := configstate.NewTransaction(s.mockContext.State())
+ transaction.Set("test-snap", "test-key1", "test-value1")
+ transaction.Set("test-snap", "test-key2", "test-value2")
+ transaction.Commit()
+ s.mockContext.State().Unlock()
+
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key2=test-value3"})
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+
+ // Notify the context that we're done. This should save the config.
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+ c.Check(s.mockContext.Done(), IsNil)
+
+ // Verify that the global config has been updated, but only test-key2
+ transaction = configstate.NewTransaction(s.mockContext.State())
+ var value string
+ c.Check(transaction.Get("test-snap", "test-key1", &value), IsNil)
+ c.Check(value, Equals, "test-value1")
+ c.Check(transaction.Get("test-snap", "test-key2", &value), IsNil)
+ c.Check(value, Equals, "test-value3")
+}
+
+func (s *setSuite) TestCommandWithoutContext(c *C) {
+ _, _, err := ctlcmd.Run(nil, []string{"set", "foo=bar"})
+ c.Check(err, ErrorMatches, ".*cannot set without a context.*")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package hookstate implements the manager and state aspects responsible for
+// the running of hooks.
+package hookstate
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "regexp"
+ "sync"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+// HookManager is responsible for the maintenance of hooks in the system state.
+// It runs hooks when they're requested, assuming they're present in the given
+// snap. Otherwise they're skipped with no error.
+type HookManager struct {
+ state *state.State
+ runner *state.TaskRunner
+ repository *repository
+
+ contextsMutex sync.RWMutex
+ contexts map[string]*Context
+}
+
+// Handler is the interface a client must satify to handle hooks.
+type Handler interface {
+ // Before is called right before the hook is to be run.
+ Before() error
+
+ // Done is called right after the hook has finished successfully.
+ Done() error
+
+ // Error is called if the hook encounters an error while running.
+ Error(err error) error
+}
+
+// HandlerGenerator is the function signature required to register for hooks.
+type HandlerGenerator func(*Context) Handler
+
+// HookSetup is a reference to a hook within a specific snap.
+type HookSetup struct {
+ Snap string `json:"snap"`
+ Revision snap.Revision `json:"revision"`
+ Hook string `json:"hook"`
+ Optional bool `json:"optional,omitempty"`
+}
+
+// Manager returns a new HookManager.
+func Manager(s *state.State) (*HookManager, error) {
+ runner := state.NewTaskRunner(s)
+ manager := &HookManager{
+ state: s,
+ runner: runner,
+ repository: newRepository(),
+ contexts: make(map[string]*Context),
+ }
+
+ runner.AddHandler("run-hook", manager.doRunHook, nil)
+
+ return manager, nil
+}
+
+// HookTask returns a task that will run the specified hook. Note that the
+// initial context must properly marshal and unmarshal with encoding/json.
+func HookTask(st *state.State, summary string, setup *HookSetup, contextData map[string]interface{}) *state.Task {
+ task := st.NewTask("run-hook", summary)
+ task.Set("hook-setup", setup)
+
+ // Initial data for Context.Get/Set.
+ if len(contextData) > 0 {
+ task.Set("hook-context", contextData)
+ }
+ return task
+}
+
+// Register registers a function to create Handler values whenever hooks
+// matching the provided pattern are run.
+func (m *HookManager) Register(pattern *regexp.Regexp, generator HandlerGenerator) {
+ m.repository.addHandlerGenerator(pattern, generator)
+}
+
+// Ensure implements StateManager.Ensure.
+func (m *HookManager) Ensure() error {
+ m.runner.Ensure()
+ return nil
+}
+
+// Wait implements StateManager.Wait.
+func (m *HookManager) Wait() {
+ m.runner.Wait()
+}
+
+// Stop implements StateManager.Stop.
+func (m *HookManager) Stop() {
+ m.runner.Stop()
+}
+
+// Context obtains the context for the given context ID.
+func (m *HookManager) Context(contextID string) (*Context, error) {
+ m.contextsMutex.RLock()
+ defer m.contextsMutex.RUnlock()
+
+ context, ok := m.contexts[contextID]
+ if !ok {
+ return nil, fmt.Errorf("no context for ID: %q", contextID)
+ }
+
+ return context, nil
+}
+
+func hookSetup(task *state.Task) (*HookSetup, *snapstate.SnapState, error) {
+ var hooksup HookSetup
+ err := task.Get("hook-setup", &hooksup)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot extract hook setup from task: %s", err)
+ }
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(task.State(), hooksup.Snap, &snapst)
+ if err == state.ErrNoState {
+ return nil, nil, fmt.Errorf("cannot find %q snap", hooksup.Snap)
+ }
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot handle %q snap: %v", hooksup.Snap, err)
+ }
+
+ return &hooksup, &snapst, nil
+}
+
+// doRunHook actually runs the hook that was requested.
+//
+// Note that this method is synchronous, as the task is already running in a
+// goroutine.
+func (m *HookManager) doRunHook(task *state.Task, tomb *tomb.Tomb) error {
+ task.State().Lock()
+ hooksup, snapst, err := hookSetup(task)
+ task.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return fmt.Errorf("cannot read %q snap details: %v", hooksup.Snap, err)
+ }
+
+ hookExists := info.Hooks[hooksup.Hook] != nil
+ if !hookExists && !hooksup.Optional {
+ return fmt.Errorf("snap %q has no %q hook", hooksup.Snap, hooksup.Hook)
+ }
+
+ context, err := NewContext(task, hooksup, nil)
+ if err != nil {
+ return err
+ }
+
+ // Obtain a handler for this hook. The repository returns a list since it's
+ // possible for regular expressions to overlap, but multiple handlers is an
+ // error (as is no handler).
+ handlers := m.repository.generateHandlers(context)
+ handlersCount := len(handlers)
+ if handlersCount == 0 {
+ return fmt.Errorf("internal error: no registered handlers for hook %q", hooksup.Hook)
+ }
+ if handlersCount > 1 {
+ return fmt.Errorf("internal error: %d handlers registered for hook %q, expected 1", handlersCount, hooksup.Hook)
+ }
+
+ context.handler = handlers[0]
+
+ contextID := context.ID()
+ m.contextsMutex.Lock()
+ m.contexts[contextID] = context
+ m.contextsMutex.Unlock()
+
+ defer func() {
+ m.contextsMutex.Lock()
+ delete(m.contexts, contextID)
+ m.contextsMutex.Unlock()
+ }()
+
+ if err = context.Handler().Before(); err != nil {
+ return err
+ }
+
+ if hookExists {
+ output, err := runHook(context, tomb)
+ if err != nil {
+ err = osutil.OutputErr(output, err)
+ if handlerErr := context.Handler().Error(err); handlerErr != nil {
+ return handlerErr
+ }
+
+ return err
+ }
+ }
+
+ if err = context.Handler().Done(); err != nil {
+ return err
+ }
+
+ context.Lock()
+ defer context.Unlock()
+ if err = context.Done(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func runHookImpl(c *Context, tomb *tomb.Tomb) ([]byte, error) {
+ return runHookAndWait(c.SnapName(), c.SnapRevision(), c.HookName(), c.ID(), tomb)
+}
+
+var runHook = runHookImpl
+
+// MockRunHook mocks the actual invocation of hooks for tests.
+func MockRunHook(hookInvoke func(c *Context, tomb *tomb.Tomb) ([]byte, error)) (restore func()) {
+ oldRunHook := runHook
+ runHook = hookInvoke
+ return func() {
+ runHook = oldRunHook
+ }
+}
+
+func runHookAndWait(snapName string, revision snap.Revision, hookName, hookContext string, tomb *tomb.Tomb) ([]byte, error) {
+ command := exec.Command("snap", "run", "--hook", hookName, "-r", revision.String(), snapName)
+
+ // Make sure the hook has its context defined so it can communicate via the
+ // REST API.
+ command.Env = append(os.Environ(), fmt.Sprintf("SNAP_CONTEXT=%s", hookContext))
+
+ // Make sure we can obtain stdout and stderror. Same buffer so they're
+ // combined.
+ buffer := bytes.NewBuffer(nil)
+ command.Stdout = buffer
+ command.Stderr = buffer
+
+ // Actually run the hook.
+ if err := command.Start(); err != nil {
+ return nil, err
+ }
+
+ hookCompleted := make(chan struct{})
+ var hookError error
+ go func() {
+ // Wait for hook to complete
+ hookError = command.Wait()
+ close(hookCompleted)
+ }()
+
+ select {
+ // Hook completed; it may or may not have been successful.
+ case <-hookCompleted:
+ return buffer.Bytes(), hookError
+
+ // Hook was aborted.
+ case <-tomb.Dying():
+ if err := command.Process.Kill(); err != nil {
+ return nil, fmt.Errorf("cannot abort hook %q: %s", hookName, err)
+ }
+ return nil, fmt.Errorf("hook %q aborted", hookName)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hookstate_test
+
+import (
+ "encoding/json"
+ "regexp"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func TestHookManager(t *testing.T) { TestingT(t) }
+
+type hookManagerSuite struct {
+ state *state.State
+ manager *hookstate.HookManager
+ context *hookstate.Context
+ mockHandler *hooktest.MockHandler
+ task *state.Task
+ change *state.Change
+ command *testutil.MockCmd
+}
+
+var _ = Suite(&hookManagerSuite{})
+
+var snapYaml = `
+name: test-snap
+version: 1.0
+hooks:
+ configure:
+ prepare-device:
+`
+var snapContents = ""
+
+func (s *hookManagerSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ s.state = state.New(nil)
+ manager, err := hookstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.manager = manager
+
+ hooksup := &hookstate.HookSetup{
+ Snap: "test-snap",
+ Hook: "configure",
+ Revision: snap.R(1),
+ }
+
+ initialContext := map[string]interface{}{
+ "test-key": "test-value",
+ }
+
+ s.state.Lock()
+ s.task = hookstate.HookTask(s.state, "test summary", hooksup, initialContext)
+ c.Assert(s.task, NotNil, Commentf("Expected HookTask to return a task"))
+
+ s.change = s.state.NewChange("kind", "summary")
+ s.change.AddTask(s.task)
+
+ sideInfo := &snap.SideInfo{RealName: "test-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
+ snaptest.MockSnap(c, snapYaml, snapContents, sideInfo)
+ snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfo},
+ Current: snap.R(1),
+ })
+ s.state.Unlock()
+
+ s.command = testutil.MockCommand(c, "snap", "")
+
+ s.context = nil
+ s.mockHandler = hooktest.NewMockHandler()
+ s.manager.Register(regexp.MustCompile("configure"), func(context *hookstate.Context) hookstate.Handler {
+ s.context = context
+ return s.mockHandler
+ })
+}
+
+func (s *hookManagerSuite) TearDownTest(c *C) {
+ s.manager.Stop()
+ dirs.SetRootDir("")
+}
+
+func (s *hookManagerSuite) TestSmoke(c *C) {
+ s.manager.Ensure()
+ s.manager.Wait()
+}
+
+func (s *hookManagerSuite) TestHookSetupJsonMarshal(c *C) {
+ hookSetup := &hookstate.HookSetup{Snap: "snap-name", Revision: snap.R(1), Hook: "hook-name"}
+ out, err := json.Marshal(hookSetup)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "{\"snap\":\"snap-name\",\"revision\":\"1\",\"hook\":\"hook-name\"}")
+}
+
+func (s *hookManagerSuite) TestHookSetupJsonUnmarshal(c *C) {
+ out, err := json.Marshal(hookstate.HookSetup{Snap: "snap-name", Revision: snap.R(1), Hook: "hook-name"})
+ c.Assert(err, IsNil)
+
+ var setup hookstate.HookSetup
+ err = json.Unmarshal(out, &setup)
+ c.Assert(err, IsNil)
+ c.Check(setup.Snap, Equals, "snap-name")
+ c.Check(setup.Revision, Equals, snap.R(1))
+ c.Check(setup.Hook, Equals, "hook-name")
+}
+
+func (s *hookManagerSuite) TestHookTask(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ hooksup := &hookstate.HookSetup{
+ Snap: "test-snap",
+ Hook: "configure",
+ Revision: snap.R(1),
+ }
+
+ task := hookstate.HookTask(s.state, "test summary", hooksup, nil)
+ c.Check(task.Kind(), Equals, "run-hook")
+
+ var setup hookstate.HookSetup
+ err := task.Get("hook-setup", &setup)
+ c.Check(err, IsNil)
+ c.Check(setup.Snap, Equals, "test-snap")
+ c.Check(setup.Revision, Equals, snap.R(1))
+ c.Check(setup.Hook, Equals, "configure")
+}
+
+func (s *hookManagerSuite) TestHookTaskEnsure(c *C) {
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Assert(s.context, NotNil, Commentf("Expected handler generator to be called with a valid context"))
+ c.Check(s.context.SnapName(), Equals, "test-snap")
+ c.Check(s.context.SnapRevision(), Equals, snap.R(1))
+ c.Check(s.context.HookName(), Equals, "configure")
+
+ c.Check(s.command.Calls(), DeepEquals, [][]string{{
+ "snap", "run", "--hook", "configure", "-r", "1", "test-snap",
+ }})
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, true)
+ c.Check(s.mockHandler.ErrorCalled, Equals, false)
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.DoneStatus)
+ c.Check(s.change.Status(), Equals, state.DoneStatus)
+}
+
+func (s *hookManagerSuite) TestHookTaskInitializesContext(c *C) {
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ var value string
+ c.Assert(s.context, NotNil, Commentf("Expected handler generator to be called with a valid context"))
+ s.context.Lock()
+ defer s.context.Unlock()
+ c.Check(s.context.Get("test-key", &value), IsNil, Commentf("Expected context to be initialized"))
+ c.Check(value, Equals, "test-value")
+}
+
+func (s *hookManagerSuite) TestHookTaskHandlesHookError(c *C) {
+ // Force the snap command to exit 1, and print something to stderr
+ s.command = testutil.MockCommand(
+ c, "snap", ">&2 echo 'hook failed at user request'; exit 1")
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.ErrorCalled, Equals, true)
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, ".*failed at user request.*")
+}
+
+func (s *hookManagerSuite) TestHookTaskCanKillHook(c *C) {
+ // Force the snap command to hang
+ s.command = testutil.MockCommand(c, "snap", "while true; do sleep 1; done")
+
+ s.manager.Ensure()
+ completed := make(chan struct{})
+ go func() {
+ s.manager.Wait()
+ close(completed)
+ }()
+
+ // Abort the change, which should kill the hanging hook, and wait for the
+ // task to complete.
+ s.state.Lock()
+ s.change.Abort()
+ s.state.Unlock()
+ s.manager.Ensure()
+ <-completed
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.ErrorCalled, Equals, true)
+ c.Check(s.mockHandler.Err, ErrorMatches, ".*hook \"configure\" aborted.*")
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*hook "configure" aborted.*`)
+}
+
+func (s *hookManagerSuite) TestHookTaskCorrectlyIncludesContext(c *C) {
+ // Force the snap command to exit with a failure and print to stderr so we
+ // can catch and verify it.
+ s.command = testutil.MockCommand(
+ c, "snap", ">&2 echo \"SNAP_CONTEXT=$SNAP_CONTEXT\"; exit 1")
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.ErrorCalled, Equals, true)
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*SNAP_CONTEXT=\S+`)
+}
+
+func (s *hookManagerSuite) TestHookTaskHandlerBeforeError(c *C) {
+ s.mockHandler.BeforeError = true
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.ErrorCalled, Equals, false)
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*Before failed at user request.*`)
+}
+
+func (s *hookManagerSuite) TestHookTaskHandlerDoneError(c *C) {
+ s.mockHandler.DoneError = true
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, true)
+ c.Check(s.mockHandler.ErrorCalled, Equals, false)
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*Done failed at user request.*`)
+}
+
+func (s *hookManagerSuite) TestHookTaskHandlerErrorError(c *C) {
+ s.mockHandler.ErrorError = true
+
+ // Force the snap command to simply exit 1, so the handler Error() runs
+ s.command = testutil.MockCommand(c, "snap", "exit 1")
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.ErrorCalled, Equals, true)
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*Error failed at user request.*`)
+}
+
+func (s *hookManagerSuite) TestHookWithoutHandlerIsError(c *C) {
+ hooksup := &hookstate.HookSetup{
+ Snap: "test-snap",
+ Hook: "prepare-device",
+ Revision: snap.R(1),
+ }
+ s.state.Lock()
+ s.task.Set("hook-setup", hooksup)
+ s.state.Unlock()
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*no registered handlers for hook "prepare-device".*`)
+}
+
+func (s *hookManagerSuite) TestHookWithMultipleHandlersIsError(c *C) {
+ // Register multiple times for this hook
+ s.manager.Register(regexp.MustCompile("configure"), func(context *hookstate.Context) hookstate.Handler {
+ return hooktest.NewMockHandler()
+ })
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+
+ checkTaskLogContains(c, s.task, `.*2 handlers registered for hook "configure".*`)
+}
+
+func (s *hookManagerSuite) TestHookWithoutHookIsError(c *C) {
+ hooksup := &hookstate.HookSetup{
+ Snap: "test-snap",
+ Hook: "missing-hook",
+ }
+ s.state.Lock()
+ s.task.Set("hook-setup", hooksup)
+ s.state.Unlock()
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.ErrorStatus)
+ c.Check(s.change.Status(), Equals, state.ErrorStatus)
+ checkTaskLogContains(c, s.task, `.*snap "test-snap" has no "missing-hook" hook`)
+}
+
+func (s *hookManagerSuite) TestHookWithoutHookOptional(c *C) {
+ s.manager.Register(regexp.MustCompile("missing-hook"), func(context *hookstate.Context) hookstate.Handler {
+ return s.mockHandler
+ })
+
+ hooksup := &hookstate.HookSetup{
+ Snap: "test-snap",
+ Hook: "missing-hook",
+ Optional: true,
+ }
+ s.state.Lock()
+ s.task.Set("hook-setup", hooksup)
+ s.state.Unlock()
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+ c.Check(s.mockHandler.DoneCalled, Equals, true)
+ c.Check(s.mockHandler.ErrorCalled, Equals, false)
+
+ c.Check(s.command.Calls(), IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(s.task.Kind(), Equals, "run-hook")
+ c.Check(s.task.Status(), Equals, state.DoneStatus)
+ c.Check(s.change.Status(), Equals, state.DoneStatus)
+
+ c.Logf("Task log:\n%s\n", s.task.Log())
+}
+
+func checkTaskLogContains(c *C, task *state.Task, pattern string) {
+ exp := regexp.MustCompile(pattern)
+ found := false
+ for _, message := range task.Log() {
+ if exp.MatchString(message) {
+ found = true
+ }
+ }
+
+ c.Check(found, Equals, true, Commentf("Expected to find regex %q in task log: %v", pattern, task.Log()))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hooktest
+
+import "fmt"
+
+// MockHandler is a mock hookstate.Handler.
+type MockHandler struct {
+ BeforeCalled bool
+ BeforeError bool
+
+ DoneCalled bool
+ DoneError bool
+
+ ErrorCalled bool
+ ErrorError bool
+ Err error
+}
+
+// NewMockHandler returns a new MockHandler.
+func NewMockHandler() *MockHandler {
+ return &MockHandler{}
+}
+
+// Before satisfies hookstate.Handler.Before
+func (h *MockHandler) Before() error {
+ h.BeforeCalled = true
+ if h.BeforeError {
+ return fmt.Errorf("Before failed at user request")
+ }
+ return nil
+}
+
+// Done satisfies hookstate.Handler.Done
+func (h *MockHandler) Done() error {
+ h.DoneCalled = true
+ if h.DoneError {
+ return fmt.Errorf("Done failed at user request")
+ }
+ return nil
+}
+
+// Error satisfies hookstate.Handler.Error
+func (h *MockHandler) Error(err error) error {
+ h.Err = err
+ h.ErrorCalled = true
+ if h.ErrorError {
+ return fmt.Errorf("Error failed at user request")
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hooktest_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type hooktestSuite struct {
+ mockHandler *hooktest.MockHandler
+}
+
+var _ = Suite(&hooktestSuite{})
+
+func (s *hooktestSuite) SetUpTest(c *C) {
+ s.mockHandler = hooktest.NewMockHandler()
+}
+
+func (s *hooktestSuite) TestBefore(c *C) {
+ c.Check(s.mockHandler.BeforeCalled, Equals, false)
+ c.Check(s.mockHandler.Before(), IsNil)
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+}
+
+func (s *hooktestSuite) TestBeforeError(c *C) {
+ s.mockHandler.BeforeError = true
+ c.Check(s.mockHandler.Before(), NotNil)
+ c.Check(s.mockHandler.BeforeCalled, Equals, true)
+}
+
+func (s *hooktestSuite) TestDone(c *C) {
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.Done(), IsNil)
+ c.Check(s.mockHandler.DoneCalled, Equals, true)
+}
+
+func (s *hooktestSuite) TestDoneError(c *C) {
+ s.mockHandler.DoneError = true
+ c.Check(s.mockHandler.Done(), NotNil)
+ c.Check(s.mockHandler.DoneCalled, Equals, true)
+}
+
+func (s *hooktestSuite) TestError(c *C) {
+ err := fmt.Errorf("test error")
+ c.Check(s.mockHandler.ErrorCalled, Equals, false)
+ c.Check(s.mockHandler.Error(err), IsNil)
+ c.Check(s.mockHandler.ErrorCalled, Equals, true)
+ c.Check(s.mockHandler.Err, Equals, err)
+}
+
+func (s *hooktestSuite) TestErrorError(c *C) {
+ s.mockHandler.ErrorError = true
+ err := fmt.Errorf("test error")
+ c.Check(s.mockHandler.Error(err), NotNil)
+ c.Check(s.mockHandler.ErrorCalled, Equals, true)
+ c.Check(s.mockHandler.Err, Equals, err)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hookstate
+
+import (
+ "regexp"
+ "sync"
+)
+
+// repository stores all registered handler generators, and generates registered
+// handlers.
+type repository struct {
+ mutex sync.RWMutex
+ generators []patternGeneratorPair
+}
+
+// patternGeneratorPair contains a hook handler generator and its corresponding
+// regex pattern for what hook name should cause it to be called.
+type patternGeneratorPair struct {
+ pattern *regexp.Regexp
+ generator HandlerGenerator
+}
+
+// NewRepository creates an empty handler generator repository.
+func newRepository() *repository {
+ return &repository{}
+}
+
+// AddHandler adds the provided handler generator to the repository.
+func (r *repository) addHandlerGenerator(pattern *regexp.Regexp, generator HandlerGenerator) {
+ r.mutex.Lock()
+ defer r.mutex.Unlock()
+
+ r.generators = append(r.generators, patternGeneratorPair{
+ pattern: pattern,
+ generator: generator,
+ })
+}
+
+// GenerateHandlers calls the handler generators whose patterns match the
+// hook name contained within the provided context, and returns the resulting
+// handlers.
+func (r *repository) generateHandlers(context *Context) []Handler {
+ hookName := context.HookName()
+ var handlers []Handler
+
+ r.mutex.RLock()
+ defer r.mutex.RUnlock()
+
+ for _, pair := range r.generators {
+ if pair.pattern.MatchString(hookName) {
+ handlers = append(handlers, pair.generator(context))
+ }
+ }
+
+ return handlers
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package hookstate
+
+import (
+ "regexp"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type repositorySuite struct{}
+
+var _ = Suite(&repositorySuite{})
+
+func (s *repositorySuite) TestAddHandlerGenerator(c *C) {
+ repository := newRepository()
+
+ var calledContext *Context
+ mockHandlerGenerator := func(context *Context) Handler {
+ calledContext = context
+ return hooktest.NewMockHandler()
+ }
+
+ // Verify that a handler generator can be added to the repository
+ repository.addHandlerGenerator(regexp.MustCompile("test-hook"), mockHandlerGenerator)
+
+ state := state.New(nil)
+ state.Lock()
+ task := state.NewTask("test-task", "my test task")
+ setup := &HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
+ context := &Context{task: task, setup: setup}
+ state.Unlock()
+
+ c.Assert(context, NotNil)
+
+ // Verify that the handler can be generated
+ handlers := repository.generateHandlers(context)
+ c.Check(handlers, HasLen, 1)
+ c.Check(calledContext, DeepEquals, context)
+
+ // Add another handler
+ repository.addHandlerGenerator(regexp.MustCompile(".*-hook"), mockHandlerGenerator)
+
+ // Verify that two handlers are generated for the test-hook, now
+ handlers = repository.generateHandlers(context)
+ c.Check(handlers, HasLen, 2)
+ c.Check(calledContext, DeepEquals, context)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacestate
+
+import (
+ "fmt"
+ "sort"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/policy"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+// confinementOptions returns interfaces.ConfinementOptions from snapstate.Flags.
+func confinementOptions(flags snapstate.Flags) interfaces.ConfinementOptions {
+ return interfaces.ConfinementOptions{
+ DevMode: flags.DevMode,
+ JailMode: flags.JailMode,
+ Classic: flags.Classic,
+ }
+}
+
+func (m *InterfaceManager) setupAffectedSnaps(task *state.Task, affectingSnap string, affectedSnaps []string) error {
+ st := task.State()
+
+ // Setup security of the affected snaps.
+ for _, affectedSnapName := range affectedSnaps {
+ // the snap that triggered the change needs to be skipped
+ if affectedSnapName == affectingSnap {
+ continue
+ }
+ var snapst snapstate.SnapState
+ if err := snapstate.Get(st, affectedSnapName, &snapst); err != nil {
+ return err
+ }
+ affectedSnapInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ snap.AddImplicitSlots(affectedSnapInfo)
+ opts := confinementOptions(snapst.Flags)
+ if err := setupSnapSecurity(task, affectedSnapInfo, opts, m.repo); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *InterfaceManager) doSetupProfiles(task *state.Task, tomb *tomb.Tomb) error {
+ task.State().Lock()
+ defer task.State().Unlock()
+
+ // Get snap.Info from bits handed by the snap manager.
+ snapsup, err := snapstate.TaskSnapSetup(task)
+ if err != nil {
+ return err
+ }
+
+ snapInfo, err := snap.ReadInfo(snapsup.Name(), snapsup.SideInfo)
+ if err != nil {
+ return err
+ }
+
+ opts := confinementOptions(snapsup.Flags)
+ return m.setupProfilesForSnap(task, tomb, snapInfo, opts)
+}
+
+func (m *InterfaceManager) setupProfilesForSnap(task *state.Task, _ *tomb.Tomb, snapInfo *snap.Info, opts interfaces.ConfinementOptions) error {
+ snap.AddImplicitSlots(snapInfo)
+ snapName := snapInfo.Name()
+
+ // The snap may have been updated so perform the following operation to
+ // ensure that we are always working on the correct state:
+ //
+ // - disconnect all connections to/from the given snap
+ // - remembering the snaps that were affected by this operation
+ // - remove the (old) snap from the interfaces repository
+ // - add the (new) snap to the interfaces repository
+ // - restore connections based on what is kept in the state
+ // - if a connection cannot be restored then remove it from the state
+ // - setup the security of all the affected snaps
+ affectedSnaps, err := m.repo.DisconnectSnap(snapName)
+ if err != nil {
+ return err
+ }
+ // XXX: what about snap renames? We should remove the old name (or switch
+ // to IDs in the interfaces repository)
+ if err := m.repo.RemoveSnap(snapName); err != nil {
+ return err
+ }
+ if err := m.repo.AddSnap(snapInfo); err != nil {
+ if _, ok := err.(*interfaces.BadInterfacesError); ok {
+ task.Logf("%s", err)
+ } else {
+ return err
+ }
+ }
+ if err := m.reloadConnections(snapName); err != nil {
+ return err
+ }
+ // FIXME: here we should not reconnect auto-connect plug/slot
+ // pairs that were explicitly disconnected by the user
+ if err := m.autoConnect(task, snapName, nil); err != nil {
+ return err
+ }
+ if err := setupSnapSecurity(task, snapInfo, opts, m.repo); err != nil {
+ return err
+ }
+
+ return m.setupAffectedSnaps(task, snapName, affectedSnaps)
+}
+
+func (m *InterfaceManager) doRemoveProfiles(task *state.Task, tomb *tomb.Tomb) error {
+ st := task.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // Get SnapSetup for this snap. This is gives us the name of the snap.
+ snapSetup, err := snapstate.TaskSnapSetup(task)
+ if err != nil {
+ return err
+ }
+ snapName := snapSetup.Name()
+
+ return m.removeProfilesForSnap(task, tomb, snapName)
+}
+
+func (m *InterfaceManager) removeProfilesForSnap(task *state.Task, _ *tomb.Tomb, snapName string) error {
+ // Disconnect the snap entirely.
+ // This is required to remove the snap from the interface repository.
+ // The returned list of affected snaps will need to have its security setup
+ // to reflect the change.
+ affectedSnaps, err := m.repo.DisconnectSnap(snapName)
+ if err != nil {
+ return err
+ }
+ if err := m.setupAffectedSnaps(task, snapName, affectedSnaps); err != nil {
+ return err
+ }
+
+ // Remove the snap from the interface repository.
+ // This discards all the plugs and slots belonging to that snap.
+ if err := m.repo.RemoveSnap(snapName); err != nil {
+ return err
+ }
+
+ // Remove security artefacts of the snap.
+ if err := removeSnapSecurity(task, snapName); err != nil {
+ // TODO: how long to wait?
+ return &state.Retry{}
+ }
+
+ return nil
+}
+
+func (m *InterfaceManager) undoSetupProfiles(task *state.Task, tomb *tomb.Tomb) error {
+ st := task.State()
+ st.Lock()
+ defer st.Unlock()
+
+ snapsup, err := snapstate.TaskSnapSetup(task)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+
+ // Get the name from SnapSetup and use it to find the current SideInfo
+ // about the snap, if there is one.
+ var snapst snapstate.SnapState
+ err = snapstate.Get(st, snapName, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ sideInfo := snapst.CurrentSideInfo()
+ if sideInfo == nil {
+ // The snap was not installed before so undo should remove security profiles.
+ return m.removeProfilesForSnap(task, tomb, snapName)
+ } else {
+ // The snap was installed before so undo should setup the old security profiles.
+ snapInfo, err := snap.ReadInfo(snapName, sideInfo)
+ if err != nil {
+ return err
+ }
+ opts := confinementOptions(snapst.Flags)
+ return m.setupProfilesForSnap(task, tomb, snapInfo, opts)
+ }
+}
+
+func (m *InterfaceManager) doDiscardConns(task *state.Task, _ *tomb.Tomb) error {
+ st := task.State()
+ st.Lock()
+ defer st.Unlock()
+
+ snapSetup, err := snapstate.TaskSnapSetup(task)
+ if err != nil {
+ return err
+ }
+
+ snapName := snapSetup.Name()
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(st, snapName, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+
+ if err == nil && len(snapst.Sequence) != 0 {
+ return fmt.Errorf("cannot discard connections for snap %q while it is present", snapName)
+ }
+ conns, err := getConns(st)
+ if err != nil {
+ return err
+ }
+ removed := make(map[string]connState)
+ for id := range conns {
+ plugRef, slotRef, err := parseConnID(id)
+ if err != nil {
+ return err
+ }
+ if plugRef.Snap == snapName || slotRef.Snap == snapName {
+ removed[id] = conns[id]
+ delete(conns, id)
+ }
+ }
+ task.Set("removed", removed)
+ setConns(st, conns)
+ return nil
+}
+
+func (m *InterfaceManager) undoDiscardConns(task *state.Task, _ *tomb.Tomb) error {
+ st := task.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var removed map[string]connState
+ err := task.Get("removed", &removed)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+
+ conns, err := getConns(st)
+ if err != nil {
+ return err
+ }
+
+ for id, connState := range removed {
+ conns[id] = connState
+ }
+ setConns(st, conns)
+ task.Set("removed", nil)
+ return nil
+}
+
+func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error {
+ st := task.State()
+ st.Lock()
+ defer st.Unlock()
+
+ plugRef, slotRef, err := getPlugAndSlotRefs(task)
+ if err != nil {
+ return err
+ }
+
+ conns, err := getConns(st)
+ if err != nil {
+ return err
+ }
+
+ connRef := interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef}
+
+ plug := m.repo.Plug(connRef.PlugRef.Snap, connRef.PlugRef.Name)
+ if plug == nil {
+ return fmt.Errorf("snap %q has no %q plug", connRef.PlugRef.Snap, connRef.PlugRef.Name)
+ }
+ var plugDecl *asserts.SnapDeclaration
+ if plug.Snap.SnapID != "" {
+ var err error
+ plugDecl, err = assertstate.SnapDeclaration(st, plug.Snap.SnapID)
+ if err != nil {
+ return fmt.Errorf("cannot find snap declaration for %q: %v", plug.Snap.Name(), err)
+ }
+ }
+
+ slot := m.repo.Slot(connRef.SlotRef.Snap, connRef.SlotRef.Name)
+ if slot == nil {
+ return fmt.Errorf("snap %q has no %q slot", connRef.SlotRef.Snap, connRef.SlotRef.Name)
+ }
+ var slotDecl *asserts.SnapDeclaration
+ if slot.Snap.SnapID != "" {
+ var err error
+ slotDecl, err = assertstate.SnapDeclaration(st, slot.Snap.SnapID)
+ if err != nil {
+ return fmt.Errorf("cannot find snap declaration for %q: %v", slot.Snap.Name(), err)
+ }
+ }
+
+ baseDecl, err := assertstate.BaseDeclaration(st)
+ if err != nil {
+ return fmt.Errorf("internal error: cannot find base declaration: %v", err)
+ }
+
+ // check the connection against the declarations' rules
+ ic := policy.ConnectCandidate{
+ Plug: plug.PlugInfo,
+ PlugSnapDeclaration: plugDecl,
+ Slot: slot.SlotInfo,
+ SlotSnapDeclaration: slotDecl,
+ BaseDeclaration: baseDecl,
+ }
+
+ // if either of plug or slot snaps don't have a declaration it
+ // means they were installed with "dangerous", so the security
+ // check should be skipped at this point.
+ if plugDecl != nil && slotDecl != nil {
+ err = ic.Check()
+ if err != nil {
+ return err
+ }
+ }
+
+ err = m.repo.Connect(connRef)
+ if err != nil {
+ return err
+ }
+
+ var plugSnapst snapstate.SnapState
+ if err := snapstate.Get(st, connRef.PlugRef.Snap, &plugSnapst); err != nil {
+ return err
+ }
+
+ var slotSnapst snapstate.SnapState
+ if err := snapstate.Get(st, connRef.SlotRef.Snap, &slotSnapst); err != nil {
+ return err
+ }
+
+ slotOpts := confinementOptions(slotSnapst.Flags)
+ if err := setupSnapSecurity(task, slot.Snap, slotOpts, m.repo); err != nil {
+ return err
+ }
+ plugOpts := confinementOptions(plugSnapst.Flags)
+ if err := setupSnapSecurity(task, plug.Snap, plugOpts, m.repo); err != nil {
+ return err
+ }
+
+ conns[connRef.ID()] = connState{Interface: plug.Interface}
+ setConns(st, conns)
+
+ return nil
+}
+
+func snapNamesFromConns(conns []interfaces.ConnRef) []string {
+ m := make(map[string]bool)
+ for _, conn := range conns {
+ m[conn.PlugRef.Snap] = true
+ m[conn.SlotRef.Snap] = true
+ }
+ l := make([]string, 0, len(m))
+ for name := range m {
+ l = append(l, name)
+ }
+ sort.Strings(l)
+ return l
+}
+
+func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error {
+ st := task.State()
+ st.Lock()
+ defer st.Unlock()
+
+ plugRef, slotRef, err := getPlugAndSlotRefs(task)
+ if err != nil {
+ return err
+ }
+
+ conns, err := getConns(st)
+ if err != nil {
+ return err
+ }
+
+ affectedConns, err := m.repo.ResolveDisconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name)
+ if err != nil {
+ return err
+ }
+ m.repo.DisconnectAll(affectedConns)
+ affectedSnaps := snapNamesFromConns(affectedConns)
+ for _, snapName := range affectedSnaps {
+ var snapst snapstate.SnapState
+ if err := snapstate.Get(st, snapName, &snapst); err != nil {
+ return err
+ }
+ snapInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ opts := confinementOptions(snapst.Flags)
+ if err := setupSnapSecurity(task, snapInfo, opts, m.repo); err != nil {
+ return &state.Retry{}
+ }
+ }
+ for _, conn := range affectedConns {
+ delete(conns, conn.ID())
+ }
+
+ setConns(st, conns)
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacestate
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/backends"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/policy"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func (m *InterfaceManager) initialize(extra []interfaces.Interface) error {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ if err := m.addInterfaces(extra); err != nil {
+ return err
+ }
+ if err := m.addSnaps(); err != nil {
+ return err
+ }
+ if err := m.reloadConnections(""); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *InterfaceManager) addInterfaces(extra []interfaces.Interface) error {
+ for _, iface := range builtin.Interfaces() {
+ if err := m.repo.AddInterface(iface); err != nil {
+ return err
+ }
+ }
+ for _, iface := range extra {
+ if err := m.repo.AddInterface(iface); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *InterfaceManager) addSnaps() error {
+ snaps, err := snapstate.ActiveInfos(m.state)
+ if err != nil {
+ return err
+ }
+ for _, snapInfo := range snaps {
+ snap.AddImplicitSlots(snapInfo)
+ if err := m.repo.AddSnap(snapInfo); err != nil {
+ logger.Noticef("%s", err)
+ }
+ }
+ return nil
+}
+
+// reloadConnections reloads connections stored in the state in the repository.
+// Using non-empty snapName the operation can be scoped to connections
+// affecting a given snap.
+func (m *InterfaceManager) reloadConnections(snapName string) error {
+ conns, err := getConns(m.state)
+ if err != nil {
+ return err
+ }
+ for id := range conns {
+ plugRef, slotRef, err := parseConnID(id)
+ if err != nil {
+ return err
+ }
+ if snapName != "" && plugRef.Snap != snapName && slotRef.Snap != snapName {
+ continue
+ }
+
+ connRef := interfaces.ConnRef{PlugRef: *plugRef, SlotRef: *slotRef}
+ err = m.repo.Connect(connRef)
+ if err != nil {
+ logger.Noticef("%s", err)
+ }
+ }
+ return nil
+}
+
+func setupSnapSecurity(task *state.Task, snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error {
+ st := task.State()
+ snapName := snapInfo.Name()
+
+ for _, backend := range backends.All {
+ st.Unlock()
+ err := backend.Setup(snapInfo, opts, repo)
+ st.Lock()
+ if err != nil {
+ task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err)
+ return err
+ }
+ }
+ return nil
+}
+
+func removeSnapSecurity(task *state.Task, snapName string) error {
+ st := task.State()
+ for _, backend := range backends.All {
+ st.Unlock()
+ err := backend.Remove(snapName)
+ st.Lock()
+ if err != nil {
+ task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err)
+ return err
+ }
+ }
+ return nil
+}
+
+type connState struct {
+ Auto bool `json:"auto,omitempty"`
+ Interface string `json:"interface,omitempty"`
+}
+
+func connID(plug *interfaces.PlugRef, slot *interfaces.SlotRef) string {
+ return fmt.Sprintf("%s:%s %s:%s", plug.Snap, plug.Name, slot.Snap, slot.Name)
+}
+
+func parseConnID(conn string) (*interfaces.PlugRef, *interfaces.SlotRef, error) {
+ parts := strings.SplitN(conn, " ", 2)
+ if len(parts) != 2 {
+ return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn)
+ }
+ plugParts := strings.SplitN(parts[0], ":", 2)
+ slotParts := strings.SplitN(parts[1], ":", 2)
+ if len(plugParts) != 2 || len(slotParts) != 2 {
+ return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn)
+ }
+ plugRef := &interfaces.PlugRef{Snap: plugParts[0], Name: plugParts[1]}
+ slotRef := &interfaces.SlotRef{Snap: slotParts[0], Name: slotParts[1]}
+ return plugRef, slotRef, nil
+}
+
+type autoConnectChecker struct {
+ st *state.State
+ cache map[string]*asserts.SnapDeclaration
+ baseDecl *asserts.BaseDeclaration
+}
+
+func newAutoConnectChecker(s *state.State) (*autoConnectChecker, error) {
+ baseDecl, err := assertstate.BaseDeclaration(s)
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot find base declaration: %v", err)
+ }
+ return &autoConnectChecker{
+ st: s,
+ cache: make(map[string]*asserts.SnapDeclaration),
+ baseDecl: baseDecl,
+ }, nil
+}
+
+func (c *autoConnectChecker) snapDeclaration(snapID string) (*asserts.SnapDeclaration, error) {
+ snapDecl := c.cache[snapID]
+ if snapDecl != nil {
+ return snapDecl, nil
+ }
+ snapDecl, err := assertstate.SnapDeclaration(c.st, snapID)
+ if err != nil {
+ return nil, err
+ }
+ c.cache[snapID] = snapDecl
+ return snapDecl, nil
+}
+
+func (c *autoConnectChecker) check(plug *interfaces.Plug, slot *interfaces.Slot) bool {
+ var plugDecl *asserts.SnapDeclaration
+ if plug.Snap.SnapID != "" {
+ var err error
+ plugDecl, err = c.snapDeclaration(plug.Snap.SnapID)
+ if err != nil {
+ logger.Noticef("error: cannot find snap declaration for %q: %v", plug.Snap.Name(), err)
+ return false
+ }
+ }
+
+ var slotDecl *asserts.SnapDeclaration
+ if slot.Snap.SnapID != "" {
+ var err error
+ slotDecl, err = c.snapDeclaration(slot.Snap.SnapID)
+ if err != nil {
+ logger.Noticef("error: cannot find snap declaration for %q: %v", slot.Snap.Name(), err)
+ return false
+ }
+ }
+
+ // check the connection against the declarations' rules
+ ic := policy.ConnectCandidate{
+ Plug: plug.PlugInfo,
+ PlugSnapDeclaration: plugDecl,
+ Slot: slot.SlotInfo,
+ SlotSnapDeclaration: slotDecl,
+ BaseDeclaration: c.baseDecl,
+ }
+
+ return ic.CheckAutoConnect() == nil
+}
+
+func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blacklist map[string]bool) error {
+ var conns map[string]connState
+ err := task.State().Get("conns", &conns)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ if conns == nil {
+ conns = make(map[string]connState)
+ }
+
+ autochecker, err := newAutoConnectChecker(task.State())
+ if err != nil {
+ return err
+ }
+
+ // XXX: quick hack, auto-connect everything
+ for _, plug := range m.repo.Plugs(snapName) {
+ if blacklist[plug.Name] {
+ continue
+ }
+ candidates := m.repo.AutoConnectCandidates(snapName, plug.Name, autochecker.check)
+ if len(candidates) != 1 {
+ continue
+ }
+ slot := candidates[0]
+ connRef := interfaces.ConnRef{
+ PlugRef: interfaces.PlugRef{Snap: snapName, Name: plug.Name},
+ SlotRef: interfaces.SlotRef{Snap: slot.Snap.Name(), Name: slot.Name},
+ }
+ if err := m.repo.Connect(connRef); err != nil {
+ task.Logf("cannot auto connect %s:%s to %s:%s: %s",
+ snapName, plug.Name, slot.Snap.Name(), slot.Name, err)
+ }
+ key := fmt.Sprintf("%s:%s %s:%s", snapName, plug.Name, slot.Snap.Name(), slot.Name)
+ conns[key] = connState{Interface: plug.Interface, Auto: true}
+ }
+ task.State().Set("conns", conns)
+ return nil
+}
+
+func getPlugAndSlotRefs(task *state.Task) (interfaces.PlugRef, interfaces.SlotRef, error) {
+ var plugRef interfaces.PlugRef
+ var slotRef interfaces.SlotRef
+ if err := task.Get("plug", &plugRef); err != nil {
+ return plugRef, slotRef, err
+ }
+ if err := task.Get("slot", &slotRef); err != nil {
+ return plugRef, slotRef, err
+ }
+ return plugRef, slotRef, nil
+}
+
+func getConns(st *state.State) (map[string]connState, error) {
+ // Get information about connections from the state
+ var conns map[string]connState
+ err := st.Get("conns", &conns)
+ if err != nil && err != state.ErrNoState {
+ return nil, fmt.Errorf("cannot obtain data about existing connections: %s", err)
+ }
+ if conns == nil {
+ conns = make(map[string]connState)
+ }
+ return conns, nil
+}
+
+func setConns(st *state.State, conns map[string]connState) {
+ st.Set("conns", conns)
+}
+
+// CheckInterfaces checks whether plugs and slots of snap are allowed for installation.
+func CheckInterfaces(st *state.State, snapInfo *snap.Info) error {
+ // XXX: AddImplicitSlots is really a brittle interface
+ snap.AddImplicitSlots(snapInfo)
+
+ if snapInfo.SnapID == "" {
+ // no SnapID means --dangerous was given, so skip interface checks
+ return nil
+ }
+
+ baseDecl, err := assertstate.BaseDeclaration(st)
+ if err != nil {
+ return fmt.Errorf("internal error: cannot find base declaration: %v", err)
+ }
+
+ snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID)
+ if err != nil {
+ return fmt.Errorf("cannot find snap declaration for %q: %v", snapInfo.Name(), err)
+ }
+
+ ic := policy.InstallCandidate{
+ Snap: snapInfo,
+ SnapDeclaration: snapDecl,
+ BaseDeclaration: baseDecl,
+ }
+
+ return ic.Check()
+}
+
+func init() {
+ // hook interface checks into snapstate installation logic
+ snapstate.AddCheckSnapCallback(func(st *state.State, snapInfo, _ *snap.Info, _ snapstate.Flags) error {
+ return CheckInterfaces(st, snapInfo)
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacestate
+
+import (
+ "regexp"
+
+ "github.com/snapcore/snapd/overlord/hookstate"
+)
+
+type prepareHandler struct{}
+
+type connectHandler struct{}
+
+func (h *prepareHandler) Before() error {
+ return nil
+}
+
+func (h *prepareHandler) Done() error {
+ return nil
+}
+
+func (h *prepareHandler) Error(err error) error {
+ return nil
+}
+
+func (h *connectHandler) Before() error {
+ return nil
+}
+
+func (h *connectHandler) Done() error {
+ return nil
+}
+
+func (h *connectHandler) Error(err error) error {
+ return nil
+}
+
+// setupHooks sets hooks of InterfaceManager up
+func setupHooks(hookMgr *hookstate.HookManager) {
+ prepareGenerator := func(context *hookstate.Context) hookstate.Handler {
+ return &prepareHandler{}
+ }
+
+ connectGenerator := func(context *hookstate.Context) hookstate.Handler {
+ return &connectHandler{}
+ }
+
+ hookMgr.Register(regexp.MustCompile("^prepare-plug-[-a-z0-9]+$"), prepareGenerator)
+ hookMgr.Register(regexp.MustCompile("^prepare-slot-[-a-z0-9]+$"), prepareGenerator)
+ hookMgr.Register(regexp.MustCompile("^connect-plug-[-a-z0-9]+$"), connectGenerator)
+ hookMgr.Register(regexp.MustCompile("^connect-slot-[-a-z0-9]+$"), connectGenerator)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package ifacestate implements the manager and state aspects
+// responsible for the maintenance of interfaces the system.
+package ifacestate
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/backends"
+ "github.com/snapcore/snapd/overlord/hookstate"
+
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// InterfaceManager is responsible for the maintenance of interfaces in
+// the system state. It maintains interface connections, and also observes
+// installed snaps to track the current set of available plugs and slots.
+type InterfaceManager struct {
+ state *state.State
+ runner *state.TaskRunner
+ repo *interfaces.Repository
+}
+
+// Manager returns a new InterfaceManager.
+// Extra interfaces can be provided for testing.
+func Manager(s *state.State, hookManager *hookstate.HookManager, extra []interfaces.Interface) (*InterfaceManager, error) {
+ // NOTE: hookManager is nil only when testing.
+ if hookManager != nil {
+ setupHooks(hookManager)
+ }
+
+ runner := state.NewTaskRunner(s)
+ m := &InterfaceManager{
+ state: s,
+ runner: runner,
+ repo: interfaces.NewRepository(),
+ }
+ if err := m.initialize(extra); err != nil {
+ return nil, err
+ }
+
+ // interface tasks might touch more than the immediate task target snap, serialize them
+ runner.SetBlocked(func(_ *state.Task, running []*state.Task) bool {
+ return len(running) != 0
+ })
+
+ runner.AddHandler("connect", m.doConnect, nil)
+ runner.AddHandler("disconnect", m.doDisconnect, nil)
+ runner.AddHandler("setup-profiles", m.doSetupProfiles, m.undoSetupProfiles)
+ runner.AddHandler("remove-profiles", m.doRemoveProfiles, m.doSetupProfiles)
+ runner.AddHandler("discard-conns", m.doDiscardConns, m.undoDiscardConns)
+
+ return m, nil
+}
+
+// Connect returns a set of tasks for connecting an interface.
+//
+func Connect(s *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) {
+ // TODO: Store the intent-to-connect in the state so that we automatically
+ // try to reconnect on reboot (reconnection can fail or can connect with
+ // different parameters so we cannot store the actual connection details).
+ plugHookSetup := &hookstate.HookSetup{
+ Snap: plugSnap,
+ Hook: "prepare-plug-" + plugName,
+ Optional: true,
+ }
+ summary := fmt.Sprintf(i18n.G("Prepare connection of plug %s:%s"), plugSnap, plugName)
+ preparePlugConnection := hookstate.HookTask(s, summary, plugHookSetup, nil)
+
+ slotHookSetup := &hookstate.HookSetup{
+ Snap: slotSnap,
+ Hook: "prepare-slot-" + slotName,
+ Optional: true,
+ }
+ summary = fmt.Sprintf(i18n.G("Prepare connection of slot %s:%s"), slotSnap, slotName)
+ prepareSlotConnection := hookstate.HookTask(s, summary, slotHookSetup, nil)
+ prepareSlotConnection.WaitFor(preparePlugConnection)
+
+ summary = fmt.Sprintf(i18n.G("Connect %s:%s to %s:%s"),
+ plugSnap, plugName, slotSnap, slotName)
+ connectInterface := s.NewTask("connect", summary)
+ connectInterface.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName})
+ connectInterface.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName})
+ connectInterface.WaitFor(prepareSlotConnection)
+
+ confirmSlotHookSetup := &hookstate.HookSetup{
+ Snap: slotSnap,
+ Hook: "connect-slot-" + slotName,
+ Optional: true,
+ }
+ summary = fmt.Sprintf(i18n.G("Confirm connection of slot %s:%s"), slotSnap, slotName)
+ confirmSlotConnection := hookstate.HookTask(s, summary, confirmSlotHookSetup, nil)
+ confirmSlotConnection.WaitFor(connectInterface)
+
+ confirmPlugHookSetup := &hookstate.HookSetup{
+ Snap: plugSnap,
+ Hook: "connect-plug-" + plugName,
+ Optional: true,
+ }
+ summary = fmt.Sprintf(i18n.G("Confirm connection of plug %s:%s"), plugSnap, plugName)
+ confirmPlugConnection := hookstate.HookTask(s, summary, confirmPlugHookSetup, nil)
+ confirmPlugConnection.WaitFor(confirmSlotConnection)
+
+ return state.NewTaskSet(preparePlugConnection, prepareSlotConnection, connectInterface, confirmSlotConnection, confirmPlugConnection), nil
+}
+
+// Disconnect returns a set of tasks for disconnecting an interface.
+func Disconnect(s *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) {
+ summary := fmt.Sprintf(i18n.G("Disconnect %s:%s from %s:%s"),
+ plugSnap, plugName, slotSnap, slotName)
+ task := s.NewTask("disconnect", summary)
+ task.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName})
+ task.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName})
+ return state.NewTaskSet(task), nil
+}
+
+// Ensure implements StateManager.Ensure.
+func (m *InterfaceManager) Ensure() error {
+ m.runner.Ensure()
+ return nil
+}
+
+// Wait implements StateManager.Wait.
+func (m *InterfaceManager) Wait() {
+ m.runner.Wait()
+}
+
+// Stop implements StateManager.Stop.
+func (m *InterfaceManager) Stop() {
+ m.runner.Stop()
+
+}
+
+// Repository returns the interface repository used internally by the manager.
+//
+// This method has two use-cases:
+// - it is needed for setting up state in daemon tests
+// - it is needed to return the set of known interfaces in the daemon api
+//
+// In the second case it is only informational and repository has internal
+// locks to ensure consistency.
+func (m *InterfaceManager) Repository() *interfaces.Repository {
+ return m.repo
+}
+
+// MockSecurityBackends mocks the list of security backends that are used for setting up security.
+//
+// This function is public because it is referenced in the daemon
+func MockSecurityBackends(be []interfaces.SecurityBackend) func() {
+ old := backends.All
+ backends.All = be
+ return func() { backends.All = old }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ifacestate_test
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/ifacetest"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/ifacestate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func TestInterfaceManager(t *testing.T) { TestingT(t) }
+
+var (
+ rootKey, _ = assertstest.GenerateKey(752)
+ storeKey, _ = assertstest.GenerateKey(752)
+)
+
+type interfaceManagerSuite struct {
+ state *state.State
+ db *asserts.Database
+ privateMgr *ifacestate.InterfaceManager
+ privateHookMgr *hookstate.HookManager
+ extraIfaces []interfaces.Interface
+ secBackend *ifacetest.TestSecurityBackend
+ restoreBackends func()
+ mockSnapCmd *testutil.MockCmd
+ storeSigning *assertstest.StoreStack
+}
+
+var _ = Suite(&interfaceManagerSuite{})
+
+func (s *interfaceManagerSuite) SetUpTest(c *C) {
+ s.storeSigning = assertstest.NewStoreStack("canonical", rootKey, storeKey)
+
+ s.mockSnapCmd = testutil.MockCommand(c, "snap", "")
+
+ dirs.SetRootDir(c.MkDir())
+ state := state.New(nil)
+ s.state = state
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.storeSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+ s.db = db
+ err = db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ assertstate.ReplaceDB(state, s.db)
+ s.state.Unlock()
+
+ s.privateHookMgr = nil
+ s.privateMgr = nil
+ s.extraIfaces = nil
+ s.secBackend = &ifacetest.TestSecurityBackend{}
+ s.restoreBackends = ifacestate.MockSecurityBackends([]interfaces.SecurityBackend{s.secBackend})
+}
+
+func (s *interfaceManagerSuite) TearDownTest(c *C) {
+ s.mockSnapCmd.Restore()
+
+ if s.privateMgr != nil {
+ s.privateMgr.Stop()
+ }
+ dirs.SetRootDir("")
+ s.restoreBackends()
+}
+
+func (s *interfaceManagerSuite) manager(c *C) *ifacestate.InterfaceManager {
+ if s.privateMgr == nil {
+ mgr, err := ifacestate.Manager(s.state, s.hookManager(c), s.extraIfaces)
+ c.Assert(err, IsNil)
+ s.privateMgr = mgr
+ }
+ return s.privateMgr
+}
+
+func (s *interfaceManagerSuite) hookManager(c *C) *hookstate.HookManager {
+ if s.privateHookMgr == nil {
+ mgr, err := hookstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.privateHookMgr = mgr
+ }
+ return s.privateHookMgr
+}
+
+func (s *interfaceManagerSuite) settle(c *C) {
+ for i := 0; i < 50; i++ {
+ s.hookManager(c).Ensure()
+ s.manager(c).Ensure()
+ s.hookManager(c).Wait()
+ s.manager(c).Wait()
+ }
+}
+
+func (s *interfaceManagerSuite) TestSmoke(c *C) {
+ mgr := s.manager(c)
+ mgr.Ensure()
+ mgr.Wait()
+}
+
+func (s *interfaceManagerSuite) TestConnectTask(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+
+ var hs hookstate.HookSetup
+ i := 0
+ task := ts.Tasks()[i]
+ c.Check(task.Kind(), Equals, "run-hook")
+ var hookSetup hookstate.HookSetup
+ err = task.Get("hook-setup", &hookSetup)
+ c.Assert(err, IsNil)
+ c.Assert(hookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "prepare-plug-plug", Optional: true})
+ i++
+ task = ts.Tasks()[i]
+ c.Check(task.Kind(), Equals, "run-hook")
+ err = task.Get("hook-setup", &hookSetup)
+ c.Assert(err, IsNil)
+ c.Assert(hookSetup, Equals, hookstate.HookSetup{Snap: "producer", Hook: "prepare-slot-slot", Optional: true})
+ i++
+ task = ts.Tasks()[i]
+ c.Assert(task.Kind(), Equals, "connect")
+ var plug interfaces.PlugRef
+ err = task.Get("plug", &plug)
+ c.Assert(err, IsNil)
+ c.Assert(plug.Snap, Equals, "consumer")
+ c.Assert(plug.Name, Equals, "plug")
+ var slot interfaces.SlotRef
+ err = task.Get("slot", &slot)
+ c.Assert(err, IsNil)
+ c.Assert(slot.Snap, Equals, "producer")
+ c.Assert(slot.Name, Equals, "slot")
+ i++
+ task = ts.Tasks()[i]
+ c.Check(task.Kind(), Equals, "run-hook")
+ err = task.Get("hook-setup", &hs)
+ c.Assert(err, IsNil)
+ c.Assert(hs, Equals, hookstate.HookSetup{Snap: "producer", Hook: "connect-slot-slot", Optional: true})
+ i++
+ task = ts.Tasks()[i]
+ c.Check(task.Kind(), Equals, "run-hook")
+ err = task.Get("hook-setup", &hs)
+ c.Assert(err, IsNil)
+ c.Assert(hs, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "connect-plug-plug", Optional: true})
+}
+
+func (s *interfaceManagerSuite) TestEnsureProcessesConnectTask(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ _ = s.manager(c)
+
+ s.state.Lock()
+ change := s.state.NewChange("kind", "summary")
+ ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot")
+
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), HasLen, 5)
+ ts.Tasks()[2].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ i := 0
+ c.Assert(change.Err(), IsNil)
+ task := change.Tasks()[i]
+ c.Check(task.Kind(), Equals, "run-hook")
+ c.Check(task.Status(), Equals, state.DoneStatus)
+ i++
+ task = change.Tasks()[i]
+ c.Check(task.Kind(), Equals, "run-hook")
+ c.Check(task.Status(), Equals, state.DoneStatus)
+ i++
+ task = change.Tasks()[i]
+ c.Check(task.Kind(), Equals, "connect")
+ c.Check(task.Status(), Equals, state.DoneStatus)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ repo := s.manager(c).Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, HasLen, 1)
+ c.Assert(slot.Connections, HasLen, 1)
+ c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"})
+ c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"})
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskCheckInterfaceMismatch(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test2"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ _ = s.manager(c)
+
+ s.state.Lock()
+ change := s.state.NewChange("kind", "summary")
+ ts, err := ifacestate.Connect(s.state, "consumer", "otherplug", "producer", "slot")
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), HasLen, 5)
+ c.Check(ts.Tasks()[2].Kind(), Equals, "connect")
+ ts.Tasks()[2].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(change.Err(), ErrorMatches, `cannot perform the following tasks:\n- Connect consumer:otherplug to producer:slot \(cannot connect plug "consumer:otherplug" \(interface "test2"\) to "producer:slot" \(interface "test".*`)
+ task := change.Tasks()[2]
+ c.Check(task.Kind(), Equals, "connect")
+ c.Check(task.Status(), Equals, state.ErrorStatus)
+ c.Check(change.Status(), Equals, state.ErrorStatus)
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskNoSuchSlot(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ _ = s.manager(c)
+
+ s.state.Lock()
+ change := s.state.NewChange("kind", "summary")
+ ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "whatslot")
+ c.Assert(err, IsNil)
+ ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(change.Err(), ErrorMatches, `cannot perform the following tasks:\n- Connect consumer:plug to producer:whatslot \(snap "producer" has no "whatslot" slot\)`)
+ c.Check(change.Status(), Equals, state.ErrorStatus)
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskNoSuchPlug(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ _ = s.manager(c)
+
+ s.state.Lock()
+ change := s.state.NewChange("kind", "summary")
+ ts, err := ifacestate.Connect(s.state, "consumer", "whatplug", "producer", "slot")
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), HasLen, 5)
+ ts.Tasks()[2].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(change.Err(), ErrorMatches, `cannot perform the following tasks:\n- Connect consumer:whatplug to producer:slot \(snap "consumer" has no "whatplug" plug\).*`)
+ c.Check(change.Status(), Equals, state.ErrorStatus)
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskCheckNotAllowed(c *C) {
+ s.testConnectTaskCheck(c, func() {
+ s.mockSnapDecl(c, "consumer", "consumer-publisher", nil)
+ s.mockSnap(c, consumerYaml)
+ s.mockSnapDecl(c, "producer", "producer-publisher", nil)
+ s.mockSnap(c, producerYaml)
+ }, func(change *state.Change) {
+ c.Check(change.Err(), ErrorMatches, `(?s).*connection not allowed by slot rule of interface "test".*`)
+ c.Check(change.Status(), Equals, state.ErrorStatus)
+
+ repo := s.manager(c).Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Check(plug.Connections, HasLen, 0)
+ c.Check(slot.Connections, HasLen, 0)
+ })
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskCheckNotAllowedButNoDecl(c *C) {
+ s.testConnectTaskCheck(c, func() {
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ }, func(change *state.Change) {
+ c.Check(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ repo := s.manager(c).Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, HasLen, 1)
+ c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"})
+ c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"})
+ })
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskCheckAllowed(c *C) {
+ s.testConnectTaskCheck(c, func() {
+ s.mockSnapDecl(c, "consumer", "one-publisher", nil)
+ s.mockSnap(c, consumerYaml)
+ s.mockSnapDecl(c, "producer", "one-publisher", nil)
+ s.mockSnap(c, producerYaml)
+ }, func(change *state.Change) {
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ repo := s.manager(c).Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, HasLen, 1)
+ c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"})
+ c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"})
+ })
+}
+
+func (s *interfaceManagerSuite) testConnectTaskCheck(c *C, setup func(), check func(*state.Change)) {
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ allow-connection:
+ plug-publisher-id:
+ - $SLOT_PUBLISHER_ID
+`))
+ defer restore()
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ setup()
+ _ = s.manager(c)
+
+ s.state.Lock()
+ change := s.state.NewChange("kind", "summary")
+ ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), HasLen, 5)
+ ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ check(change)
+}
+
+func (s *interfaceManagerSuite) TestDisconnectTask(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+
+ task := ts.Tasks()[0]
+ c.Assert(task.Kind(), Equals, "disconnect")
+ var plug interfaces.PlugRef
+ err = task.Get("plug", &plug)
+ c.Assert(err, IsNil)
+ c.Assert(plug.Snap, Equals, "consumer")
+ c.Assert(plug.Name, Equals, "plug")
+ var slot interfaces.SlotRef
+ err = task.Get("slot", &slot)
+ c.Assert(err, IsNil)
+ c.Assert(slot.Snap, Equals, "producer")
+ c.Assert(slot.Name, Equals, "slot")
+}
+
+// Disconnect works when both plug and slot are specified
+func (s *interfaceManagerSuite) TestDisconnectFull(c *C) {
+ s.testDisconnect(c, "consumer", "plug", "producer", "slot")
+}
+
+// Disconnect works when just the slot is fully specified.
+func (s *interfaceManagerSuite) TestDisconnectSlot(c *C) {
+ s.testDisconnect(c, "", "", "producer", "slot")
+}
+
+// Disconnect works when just the plug is fully specified.
+func (s *interfaceManagerSuite) TestDisconnectPlug(c *C) {
+ s.testDisconnect(c, "consumer", "plug", "", "")
+}
+
+func (s *interfaceManagerSuite) testDisconnect(c *C, plugSnap, plugName, slotSnap, slotName string) {
+ // Put two snaps in place They consumer has an plug that can be connected
+ // to slot on the producer.
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ // Put a connection in the state so that it automatically gets set up when
+ // we create the manager.
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ s.state.Unlock()
+
+ // Initialize the manager. This registers both snaps and reloads the connection.
+ mgr := s.manager(c)
+
+ // Run the disconnect task and let it finish.
+ s.state.Lock()
+ change := s.state.NewChange("disconnect", "...")
+ ts, err := ifacestate.Disconnect(s.state, plugSnap, plugName, slotSnap, slotName)
+ ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ c.Assert(err, IsNil)
+ change.AddAll(ts)
+ s.state.Unlock()
+ mgr.Ensure()
+ mgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Err(), IsNil)
+ task := change.Tasks()[0]
+ c.Check(task.Kind(), Equals, "disconnect")
+ c.Check(task.Status(), Equals, state.DoneStatus)
+
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ // Ensure that the connection has been removed from the state
+ var conns map[string]interface{}
+ err = s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, HasLen, 0)
+
+ // Ensure that the connection has been removed from the repository
+ repo := mgr.Repository()
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, HasLen, 0)
+ c.Assert(slot.Connections, HasLen, 0)
+
+ // Ensure that the backend was used to setup security of both snaps
+ c.Assert(s.secBackend.SetupCalls, HasLen, 2)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, "consumer")
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Name(), Equals, "producer")
+
+ c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{})
+ c.Check(s.secBackend.SetupCalls[1].Options, Equals, interfaces.ConfinementOptions{})
+}
+
+func (s *interfaceManagerSuite) mockIface(c *C, iface interfaces.Interface) {
+ s.extraIfaces = append(s.extraIfaces, iface)
+}
+
+func (s *interfaceManagerSuite) mockSnapDecl(c *C, name, publisher string, extraHeaders map[string]interface{}) {
+ _, err := s.db.Find(asserts.AccountType, map[string]string{
+ "account-id": publisher,
+ })
+ if err == asserts.ErrNotFound {
+ acct := assertstest.NewAccount(s.storeSigning, publisher, map[string]interface{}{
+ "account-id": publisher,
+ }, "")
+ err = s.db.Add(acct)
+ }
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-name": name,
+ "publisher-id": publisher,
+ "snap-id": (name + strings.Repeat("id", 16))[:32],
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for k, v := range extraHeaders {
+ headers[k] = v
+ }
+
+ snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = s.db.Add(snapDecl)
+ c.Assert(err, IsNil)
+}
+
+func (s *interfaceManagerSuite) mockSnap(c *C, yamlText string) *snap.Info {
+ sideInfo := &snap.SideInfo{
+ Revision: snap.R(1),
+ }
+ snapInfo := snaptest.MockSnap(c, yamlText, "", sideInfo)
+ sideInfo.RealName = snapInfo.Name()
+
+ a, err := s.db.FindMany(asserts.SnapDeclarationType, map[string]string{
+ "snap-name": sideInfo.RealName,
+ })
+ if err == nil {
+ decl := a[0].(*asserts.SnapDeclaration)
+ snapInfo.SnapID = decl.SnapID()
+ sideInfo.SnapID = decl.SnapID()
+ } else if err == asserts.ErrNotFound {
+ err = nil
+ }
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Put a side info into the state
+ snapstate.Set(s.state, snapInfo.Name(), &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfo},
+ Current: sideInfo.Revision,
+ })
+ return snapInfo
+}
+
+func (s *interfaceManagerSuite) mockUpdatedSnap(c *C, yamlText string, revision int) *snap.Info {
+ sideInfo := &snap.SideInfo{Revision: snap.R(revision)}
+ snapInfo := snaptest.MockSnap(c, yamlText, "", sideInfo)
+ sideInfo.RealName = snapInfo.Name()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Put the new revision (stored in SideInfo) into the state
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, snapInfo.Name(), &snapst)
+ c.Assert(err, IsNil)
+ snapst.Sequence = append(snapst.Sequence, sideInfo)
+ snapstate.Set(s.state, snapInfo.Name(), &snapst)
+
+ return snapInfo
+}
+
+func (s *interfaceManagerSuite) addSetupSnapSecurityChange(c *C, snapsup *snapstate.SnapSetup) *state.Change {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ task := s.state.NewTask("setup-profiles", "")
+ task.Set("snap-setup", snapsup)
+ taskset := state.NewTaskSet(task)
+ change := s.state.NewChange("test", "")
+ change.AddAll(taskset)
+ return change
+}
+
+func (s *interfaceManagerSuite) addRemoveSnapSecurityChange(c *C, snapName string) *state.Change {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ task := s.state.NewTask("remove-profiles", "")
+ snapsup := snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapName,
+ },
+ }
+ task.Set("snap-setup", snapsup)
+ taskset := state.NewTaskSet(task)
+ change := s.state.NewChange("test", "")
+ change.AddAll(taskset)
+ return change
+}
+
+func (s *interfaceManagerSuite) addDiscardConnsChange(c *C, snapName string) *state.Change {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ task := s.state.NewTask("discard-conns", "")
+ snapsup := snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapName,
+ },
+ }
+ task.Set("snap-setup", snapsup)
+ taskset := state.NewTaskSet(task)
+ change := s.state.NewChange("test", "")
+ change.AddAll(taskset)
+ return change
+}
+
+var osSnapYaml = `
+name: ubuntu-core
+version: 1
+type: os
+`
+
+var sampleSnapYaml = `
+name: snap
+version: 1
+apps:
+ app:
+ command: foo
+plugs:
+ network:
+ interface: network
+`
+
+var consumerYaml = `
+name: consumer
+version: 1
+plugs:
+ plug:
+ interface: test
+ otherplug:
+ interface: test2
+`
+
+var producerYaml = `
+name: producer
+version: 1
+slots:
+ slot:
+ interface: test
+`
+
+// The setup-profiles task will not auto-connect an plug that was previously
+// explicitly disconnected by the user.
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityHonorsDisconnect(c *C) {
+ c.Skip("feature disabled until redesign/reimpl")
+ // Add an OS snap as well as a sample snap with a "network" plug.
+ // The plug is normally auto-connected.
+ s.mockSnap(c, osSnapYaml)
+ snapInfo := s.mockSnap(c, sampleSnapYaml)
+
+ // Initialize the manager. This registers the two snaps.
+ mgr := s.manager(c)
+
+ // Run the setup-snap-security task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ // Ensure that "network" is not saved in the state as auto-connected.
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, HasLen, 0)
+
+ // Ensure that "network" is really disconnected.
+ repo := mgr.Repository()
+ plug := repo.Plug("snap", "network")
+ c.Assert(plug, Not(IsNil))
+ c.Check(plug.Connections, HasLen, 0)
+}
+
+// The setup-profiles task will auto-connect plugs with viable candidates.
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnects(c *C) {
+ // Add an OS snap.
+ s.mockSnap(c, osSnapYaml)
+
+ // Initialize the manager. This registers the OS snap.
+ mgr := s.manager(c)
+
+ // Add a sample snap with a "network" plug which should be auto-connected.
+ snapInfo := s.mockSnap(c, sampleSnapYaml)
+
+ // Run the setup-snap-security task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ // Ensure that "network" is now saved in the state as auto-connected.
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "snap:network ubuntu-core:network": map[string]interface{}{
+ "interface": "network", "auto": true,
+ },
+ })
+
+ // Ensure that "network" is really connected.
+ repo := mgr.Repository()
+ plug := repo.Plug("snap", "network")
+ c.Assert(plug, Not(IsNil))
+ c.Check(plug.Connections, HasLen, 1)
+}
+
+// The setup-profiles task will auto-connect plugs with viable candidates also condidering snap declarations.
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsDeclBased(c *C) {
+ s.testDoSetupSnapSecurityAutoConnectsDeclBased(c, true, func(conns map[string]interface{}, plug *interfaces.Plug) {
+ // Ensure that "test" plug is now saved in the state as auto-connected.
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"auto": true, "interface": "test"},
+ })
+ // Ensure that "test" is really connected.
+ c.Check(plug.Connections, HasLen, 1)
+ })
+}
+
+// The setup-profiles task will *not* auto-connect plugs with viable candidates when snap declarations are missing.
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsDeclBasedWhenMissingDecl(c *C) {
+ s.testDoSetupSnapSecurityAutoConnectsDeclBased(c, false, func(conns map[string]interface{}, plug *interfaces.Plug) {
+ // Ensure nothing is connected.
+ c.Check(conns, HasLen, 0)
+ c.Check(plug.Connections, HasLen, 0)
+ })
+}
+
+func (s *interfaceManagerSuite) testDoSetupSnapSecurityAutoConnectsDeclBased(c *C, withDecl bool, check func(map[string]interface{}, *interfaces.Plug)) {
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ allow-auto-connection:
+ plug-publisher-id:
+ - $SLOT_PUBLISHER_ID
+`))
+ defer restore()
+ // Add the producer snap
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnapDecl(c, "producer", "one-publisher", nil)
+ s.mockSnap(c, producerYaml)
+
+ // Initialize the manager. This registers the producer snap.
+ mgr := s.manager(c)
+
+ // Add a sample snap with a plug with the "test" interface which should be auto-connected.
+ if withDecl {
+ s.mockSnapDecl(c, "consumer", "one-publisher", nil)
+ }
+ snapInfo := s.mockSnap(c, consumerYaml)
+
+ // Run the setup-snap-security task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ SnapID: snapInfo.SnapID,
+ Revision: snapInfo.Revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+
+ repo := mgr.Repository()
+ plug := repo.Plug("consumer", "plug")
+ c.Assert(plug, Not(IsNil))
+
+ check(conns, plug)
+}
+
+// The setup-profiles task will only touch connection state for the task it
+// operates on or auto-connects to and will leave other state intact.
+func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionState(c *C) {
+ // Add an OS snap in place.
+ s.mockSnap(c, osSnapYaml)
+
+ // Initialize the manager. This registers the two snaps.
+ mgr := s.manager(c)
+
+ // Add a sample snap with a "network" plug which should be auto-connected.
+ snapInfo := s.mockSnap(c, sampleSnapYaml)
+
+ // Put fake information about connections for another snap into the state.
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "other-snap:network ubuntu-core:network": map[string]interface{}{
+ "interface": "network",
+ },
+ })
+ s.state.Unlock()
+
+ // Run the setup-snap-security task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ // The sample snap was auto-connected, as expected.
+ "snap:network ubuntu-core:network": map[string]interface{}{
+ "interface": "network", "auto": true,
+ },
+ // Connection state for the fake snap is preserved.
+ // The task didn't alter state of other snaps.
+ "other-snap:network ubuntu-core:network": map[string]interface{}{
+ "interface": "network",
+ },
+ })
+}
+
+// The setup-profiles task will add implicit slots necessary for the OS snap.
+func (s *interfaceManagerSuite) TestDoSetupProfilesAddsImplicitSlots(c *C) {
+ // Initialize the manager.
+ mgr := s.manager(c)
+
+ // Add an OS snap.
+ snapInfo := s.mockSnap(c, osSnapYaml)
+
+ // Run the setup-profiles task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ // Ensure that we have slots on the OS snap.
+ repo := mgr.Repository()
+ slots := repo.Slots(snapInfo.Name())
+ // NOTE: This is not an exact test as it duplicates functionality elsewhere
+ // and is was a pain to update each time. This is correctly handled by the
+ // implicit slot tests in snap/implicit_test.go
+ c.Assert(len(slots) > 18, Equals, true)
+}
+
+func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOnPlugSide(c *C) {
+ snapInfo := s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ s.testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c, snapInfo.Name(), snapInfo.Revision)
+}
+
+func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOnSlotSide(c *C) {
+ s.mockSnap(c, consumerYaml)
+ snapInfo := s.mockSnap(c, producerYaml)
+ s.testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c, snapInfo.Name(), snapInfo.Revision)
+}
+
+func (s *interfaceManagerSuite) testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c *C, snapName string, revision snap.Revision) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+
+ // Run the setup-profiles task
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapName,
+ Revision: revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ // Change succeeds
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ repo := mgr.Repository()
+
+ // Repository shows the connection
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, HasLen, 1)
+ c.Assert(slot.Connections, HasLen, 1)
+ c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"})
+ c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"})
+}
+
+// The setup-profiles task will honor snapstate.DevMode flag by storing it
+// in the SnapState.Flags and by actually setting up security
+// using that flag. Old copy of SnapState.Flag's DevMode is saved for the undo
+// handler under `old-devmode`.
+func (s *interfaceManagerSuite) TestSetupProfilesHonorsDevMode(c *C) {
+ // Put the OS snap in place.
+ mgr := s.manager(c)
+
+ // Initialize the manager. This registers the OS snap.
+ snapInfo := s.mockSnap(c, sampleSnapYaml)
+
+ // Run the setup-profiles task and let it finish.
+ // Note that the task will see SnapSetup.Flags equal to DeveloperMode.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ Flags: snapstate.Flags{DevMode: true},
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ // The snap was setup with DevModeConfinement
+ c.Assert(s.secBackend.SetupCalls, HasLen, 1)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, "snap")
+ c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{DevMode: true})
+}
+
+// setup-profiles uses the new snap.Info when setting up security for the new
+// snap when it had prior connections and DisconnectSnap() returns it as a part
+// of the affected set.
+func (s *interfaceManagerSuite) TestSetupProfilesUsesFreshSnapInfo(c *C) {
+ // Put the OS and the sample snaps in place.
+ coreSnapInfo := s.mockSnap(c, osSnapYaml)
+ oldSnapInfo := s.mockSnap(c, sampleSnapYaml)
+
+ // Put connection information between the OS snap and the sample snap.
+ // This is done so that DisconnectSnap returns both snaps as "affected"
+ // and so that the previously broken code path is exercised.
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "snap:network ubuntu-core:network": map[string]interface{}{"interface": "network"},
+ })
+ s.state.Unlock()
+
+ // Initialize the manager. This registers both of the snaps and reloads the
+ // connection between them.
+ mgr := s.manager(c)
+
+ // Put a new revision of the sample snap in place.
+ newSnapInfo := s.mockUpdatedSnap(c, sampleSnapYaml, 42)
+
+ // Sanity check, the revisions are different.
+ c.Assert(oldSnapInfo.Revision, Not(Equals), 42)
+ c.Assert(newSnapInfo.Revision, Equals, snap.R(42))
+
+ // Run the setup-profiles task for the new revision and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: newSnapInfo.Name(),
+ Revision: newSnapInfo.Revision,
+ },
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ // Ensure that both snaps were setup correctly.
+ c.Assert(s.secBackend.SetupCalls, HasLen, 2)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ // The sample snap was setup, with the correct new revision.
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, newSnapInfo.Name())
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Revision, Equals, newSnapInfo.Revision)
+ // The OS snap was setup (because it was affected).
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Name(), Equals, coreSnapInfo.Name())
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Revision, Equals, coreSnapInfo.Revision)
+}
+
+func (s *interfaceManagerSuite) TestDoDiscardConnsPlug(c *C) {
+ s.testDoDicardConns(c, "consumer")
+}
+
+func (s *interfaceManagerSuite) TestDoDiscardConnsSlot(c *C) {
+ s.testDoDicardConns(c, "producer")
+}
+
+func (s *interfaceManagerSuite) TestUndoDiscardConnsPlug(c *C) {
+ s.testUndoDicardConns(c, "consumer")
+}
+
+func (s *interfaceManagerSuite) TestUndoDiscardConnsSlot(c *C) {
+ s.testUndoDicardConns(c, "producer")
+}
+
+func (s *interfaceManagerSuite) testDoDicardConns(c *C, snapName string) {
+ s.state.Lock()
+ // Store information about a connection in the state.
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ // Store empty snap state. This snap has an empty sequence now.
+ snapstate.Set(s.state, snapName, &snapstate.SnapState{})
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+
+ // Run the discard-conns task and let it finish
+ change := s.addDiscardConnsChange(c, snapName)
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ // Information about the connection was removed
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{})
+
+ // But removed connections are preserved in the task for undo.
+ var removed map[string]interface{}
+ err = change.Tasks()[0].Get("removed", &removed)
+ c.Assert(err, IsNil)
+ c.Check(removed, DeepEquals, map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+}
+
+func (s *interfaceManagerSuite) testUndoDicardConns(c *C, snapName string) {
+ s.state.Lock()
+ // Store information about a connection in the state.
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ // Store empty snap state. This snap has an empty sequence now.
+ snapstate.Set(s.state, snapName, &snapstate.SnapState{})
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+
+ // Run the discard-conns task and let it finish
+ change := s.addDiscardConnsChange(c, snapName)
+
+ // Add a dummy task just to hold the change not ready.
+ s.state.Lock()
+ dummy := s.state.NewTask("dummy", "")
+ change.AddTask(dummy)
+ s.state.Unlock()
+
+ mgr.Ensure()
+ mgr.Wait()
+
+ s.state.Lock()
+ c.Check(change.Status(), Equals, state.DoStatus)
+ change.Abort()
+ s.state.Unlock()
+
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Assert(change.Status(), Equals, state.UndoneStatus)
+
+ // Information about the connection is intact
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+
+ var removed map[string]interface{}
+ err = change.Tasks()[0].Get("removed", &removed)
+ c.Assert(err, IsNil)
+ c.Check(removed, HasLen, 0)
+}
+
+func (s *interfaceManagerSuite) TestDoRemove(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+
+ // Run the remove-security task
+ change := s.addRemoveSnapSecurityChange(c, "consumer")
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ // Change succeeds
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ repo := mgr.Repository()
+
+ // Snap is removed from repository
+ c.Check(repo.Plug("consumer", "slot"), IsNil)
+
+ // Security of the snap was removed
+ c.Check(s.secBackend.RemoveCalls, DeepEquals, []string{"consumer"})
+
+ // Security of the related snap was configured
+ c.Check(s.secBackend.SetupCalls, HasLen, 1)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, "producer")
+
+ // Connection state was left intact
+ var conns map[string]interface{}
+ err := s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+}
+
+func (s *interfaceManagerSuite) TestConnectTracksConnectionsInState(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ _ = s.manager(c)
+
+ s.state.Lock()
+ ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), HasLen, 5)
+
+ ts.Tasks()[2].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change := s.state.NewChange("connect", "")
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+ var conns map[string]interface{}
+ err = s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{
+ "interface": "test",
+ },
+ })
+}
+
+func (s *interfaceManagerSuite) TestConnectSetsUpSecurity(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ _ = s.manager(c)
+
+ s.state.Lock()
+ ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+ ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change := s.state.NewChange("connect", "")
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ c.Assert(s.secBackend.SetupCalls, HasLen, 2)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, "producer")
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Name(), Equals, "consumer")
+
+ c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{})
+ c.Check(s.secBackend.SetupCalls[1].Options, Equals, interfaces.ConfinementOptions{})
+}
+
+func (s *interfaceManagerSuite) TestDisconnectSetsUpSecurity(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+
+ s.state.Lock()
+ ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+ ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change := s.state.NewChange("disconnect", "")
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ c.Assert(s.secBackend.SetupCalls, HasLen, 2)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, "consumer")
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Name(), Equals, "producer")
+
+ c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{})
+ c.Check(s.secBackend.SetupCalls[1].Options, Equals, interfaces.ConfinementOptions{})
+}
+
+func (s *interfaceManagerSuite) TestDisconnectTracksConnectionsInState(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+
+ s.state.Lock()
+ ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot")
+ c.Assert(err, IsNil)
+ ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer",
+ },
+ })
+
+ change := s.state.NewChange("disconnect", "")
+ change.AddAll(ts)
+ s.state.Unlock()
+
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+ var conns map[string]interface{}
+ err = s.state.Get("conns", &conns)
+ c.Assert(err, IsNil)
+ c.Check(conns, DeepEquals, map[string]interface{}{})
+}
+
+func (s *interfaceManagerSuite) TestManagerReloadsConnections(c *C) {
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ s.state.Set("conns", map[string]interface{}{
+ "consumer:plug producer:slot": map[string]interface{}{"interface": "test"},
+ })
+ s.state.Unlock()
+
+ mgr := s.manager(c)
+ repo := mgr.Repository()
+
+ plug := repo.Plug("consumer", "plug")
+ slot := repo.Slot("producer", "slot")
+ c.Assert(plug.Connections, HasLen, 1)
+ c.Assert(slot.Connections, HasLen, 1)
+ c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"})
+ c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"})
+}
+
+func (s *interfaceManagerSuite) TestSetupProfilesDevModeMultiple(c *C) {
+ mgr := s.manager(c)
+ repo := mgr.Repository()
+
+ // setup two snaps that are connected
+ siP := s.mockSnap(c, producerYaml)
+ siC := s.mockSnap(c, consumerYaml)
+ err := repo.AddInterface(&ifacetest.TestInterface{
+ InterfaceName: "test",
+ })
+ c.Assert(err, IsNil)
+ err = repo.AddSlot(&interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: siC,
+ Name: "slot",
+ Interface: "test",
+ },
+ })
+ c.Assert(err, IsNil)
+ err = repo.AddPlug(&interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: siP,
+ Name: "plug",
+ Interface: "test",
+ },
+ })
+ c.Assert(err, IsNil)
+ connRef := interfaces.ConnRef{
+ PlugRef: interfaces.PlugRef{Snap: siP.Name(), Name: "plug"},
+ SlotRef: interfaces.SlotRef{Snap: siC.Name(), Name: "slot"},
+ }
+ err = repo.Connect(connRef)
+ c.Assert(err, IsNil)
+
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: siC.Name(),
+ Revision: siC.Revision,
+ },
+ Flags: snapstate.Flags{DevMode: true},
+ })
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Check(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.DoneStatus)
+
+ // The first snap is setup in devmode, the second is not
+ c.Assert(s.secBackend.SetupCalls, HasLen, 2)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, siC.Name())
+ c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{DevMode: true})
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Name(), Equals, siP.Name())
+ c.Check(s.secBackend.SetupCalls[1].Options, Equals, interfaces.ConfinementOptions{})
+}
+
+func (s *interfaceManagerSuite) TestCheckInterfacesDeny(c *C) {
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ deny-installation: true
+`))
+ defer restore()
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ s.mockSnapDecl(c, "producer", "producer-publisher", nil)
+ snapInfo := s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo), ErrorMatches, "installation denied.*")
+}
+
+func (s *interfaceManagerSuite) TestCheckInterfacesDenySkippedIfNoDecl(c *C) {
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ deny-installation: true
+`))
+ defer restore()
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ // crucially, this test is missing this: s.mockSnapDecl(c, "producer", "producer-publisher", nil)
+ snapInfo := s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo), IsNil)
+}
+
+func (s *interfaceManagerSuite) TestCheckInterfacesAllow(c *C) {
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ deny-installation: true
+`))
+ defer restore()
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ s.mockSnapDecl(c, "producer", "producer-publisher", map[string]interface{}{
+ "slots": map[string]interface{}{
+ "test": "true",
+ },
+ })
+ snapInfo := s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo), IsNil)
+}
+
+func (s *interfaceManagerSuite) TestCheckInterfacesConsidersImplicitSlots(c *C) {
+ snapInfo := s.mockSnap(c, osSnapYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo), IsNil)
+ c.Check(snapInfo.Slots["home"], NotNil)
+}
+
+// Test that setup-snap-security gets undone correctly when a snap is installed
+// but the installation fails (the security profiles are removed).
+func (s *interfaceManagerSuite) TestUndoSetupProfilesOnInstall(c *C) {
+ // Create the interface manager
+ mgr := s.manager(c)
+
+ // Mock a snap and remove the side info from the state (it is implicitly
+ // added by mockSnap) so that we can emulate a undo during a fresh
+ // install.
+ snapInfo := s.mockSnap(c, sampleSnapYaml)
+ s.state.Lock()
+ snapstate.Set(s.state, snapInfo.Name(), nil)
+ s.state.Unlock()
+
+ // Add a change that undoes "setup-snap-security"
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ s.state.Lock()
+ change.Tasks()[0].SetStatus(state.UndoStatus)
+ s.state.Unlock()
+
+ // Turn the crank
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the change got undone.
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.UndoneStatus)
+
+ // Ensure that since we had no prior revisions of this snap installed the
+ // undo task removed the security profile from the system.
+ c.Assert(s.secBackend.SetupCalls, HasLen, 0)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 1)
+ c.Check(s.secBackend.RemoveCalls, DeepEquals, []string{snapInfo.Name()})
+}
+
+// Test that setup-snap-security gets undone correctly when a snap is refreshed
+// but the installation fails (the security profiles are restored to the old state).
+func (s *interfaceManagerSuite) TestUndoSetupProfilesOnRefresh(c *C) {
+ // Create the interface manager
+ mgr := s.manager(c)
+
+ // Mock a snap. The mockSnap call below also puts the side info into the
+ // state so it seems like it was installed already.
+ snapInfo := s.mockSnap(c, sampleSnapYaml)
+
+ // Add a change that undoes "setup-snap-security"
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.Name(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ s.state.Lock()
+ change.Tasks()[0].SetStatus(state.UndoStatus)
+ s.state.Unlock()
+
+ // Turn the crank
+ mgr.Ensure()
+ mgr.Wait()
+ mgr.Stop()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the change got undone.
+ c.Assert(change.Err(), IsNil)
+ c.Check(change.Status(), Equals, state.UndoneStatus)
+
+ // Ensure that since had a revision in the state the undo task actually
+ // setup the security of the snap we had in the state.
+ c.Assert(s.secBackend.SetupCalls, HasLen, 1)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Name(), Equals, snapInfo.Name())
+ c.Check(s.secBackend.SetupCalls[0].SnapInfo.Revision, Equals, snapInfo.Revision)
+ c.Check(s.secBackend.SetupCalls[0].Options, Equals, interfaces.ConfinementOptions{})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package overlord_test
+
+// test the various managers and their operation together through overlord
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type mgrsSuite struct {
+ tempdir string
+
+ aa *testutil.MockCmd
+ udev *testutil.MockCmd
+ umount *testutil.MockCmd
+
+ snapDiscardNs *testutil.MockCmd
+
+ prevctlCmd func(...string) ([]byte, error)
+
+ storeSigning *assertstest.StoreStack
+ restoreTrusted func()
+
+ devAcct *asserts.Account
+
+ o *overlord.Overlord
+
+ serveIDtoName map[string]string
+ serveSnapPath map[string]string
+ serveRevision map[string]string
+}
+
+var (
+ _ = Suite(&mgrsSuite{})
+ _ = Suite(&authContextSetupSuite{})
+)
+
+var (
+ rootPrivKey, _ = assertstest.GenerateKey(1024)
+ storePrivKey, _ = assertstest.GenerateKey(752)
+
+ brandPrivKey, _ = assertstest.GenerateKey(752)
+
+ develPrivKey, _ = assertstest.GenerateKey(752)
+
+ deviceKey, _ = assertstest.GenerateKey(752)
+)
+
+func (ms *mgrsSuite) SetUpTest(c *C) {
+ ms.tempdir = c.MkDir()
+ dirs.SetRootDir(ms.tempdir)
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+
+ os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+
+ // create a fake systemd environment
+ os.MkdirAll(filepath.Join(dirs.SnapServicesDir, "multi-user.target.wants"), 0755)
+
+ ms.prevctlCmd = systemd.SystemctlCmd
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ return []byte("ActiveState=inactive\n"), nil
+ }
+ ms.aa = testutil.MockCommand(c, "apparmor_parser", "")
+ ms.udev = testutil.MockCommand(c, "udevadm", "")
+ ms.umount = testutil.MockCommand(c, "umount", "")
+ ms.snapDiscardNs = testutil.MockCommand(c, "snap-discard-ns", "")
+ dirs.LibExecDir = ms.snapDiscardNs.BinDir()
+
+ ms.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+ ms.restoreTrusted = sysdb.InjectTrusted(ms.storeSigning.Trusted)
+
+ ms.devAcct = assertstest.NewAccount(ms.storeSigning, "devdevdev", map[string]interface{}{
+ "account-id": "devdevdev",
+ }, "")
+ err = ms.storeSigning.Add(ms.devAcct)
+ c.Assert(err, IsNil)
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+ ms.o = o
+ st := ms.o.State()
+ st.Lock()
+ st.Set("seeded", true)
+ st.Unlock()
+
+ ms.serveIDtoName = make(map[string]string)
+ ms.serveSnapPath = make(map[string]string)
+ ms.serveRevision = make(map[string]string)
+}
+
+func (ms *mgrsSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ ms.restoreTrusted()
+ os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
+ systemd.SystemctlCmd = ms.prevctlCmd
+ ms.udev.Restore()
+ ms.aa.Restore()
+ ms.umount.Restore()
+ ms.snapDiscardNs.Restore()
+}
+
+func makeTestSnap(c *C, snapYamlContent string) string {
+ return snaptest.MakeTestSnapWithFiles(c, snapYamlContent, nil)
+}
+
+func (ms *mgrsSuite) TestHappyLocalInstall(c *C) {
+ snapYamlContent := `name: foo
+apps:
+ bar:
+ command: bin/bar
+`
+ snapPath := makeTestSnap(c, snapYamlContent+"version: 1.0")
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.InstallPath(st, &snap.SideInfo{RealName: "foo"}, snapPath, "", snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ snap, err := snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+
+ // ensure that the binary wrapper file got generated with the right
+ // name
+ binaryWrapper := filepath.Join(dirs.SnapBinariesDir, "foo.bar")
+ c.Assert(osutil.IsSymlink(binaryWrapper), Equals, true)
+
+ // data dirs
+ c.Assert(osutil.IsDirectory(snap.DataDir()), Equals, true)
+ c.Assert(osutil.IsDirectory(snap.CommonDataDir()), Equals, true)
+
+ // snap file and its mounting
+
+ // after install the snap file is in the right dir
+ c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "foo_x1.snap")), Equals, true)
+
+ // ensure the right unit is created
+ mup := systemd.MountUnitPath("/snap/foo/x1")
+ content, err := ioutil.ReadFile(mup)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Matches, "(?ms).*^Where=/snap/foo/x1")
+ c.Assert(string(content), Matches, "(?ms).*^What=/var/lib/snapd/snaps/foo_x1.snap")
+
+}
+
+func (ms *mgrsSuite) TestHappyRemove(c *C) {
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ snapYamlContent := `name: foo
+apps:
+ bar:
+ command: bin/bar
+`
+ snapInfo := ms.installLocalTestSnap(c, snapYamlContent+"version: 1.0")
+
+ ts, err := snapstate.Remove(st, "foo", snap.R(0))
+ c.Assert(err, IsNil)
+ chg := st.NewChange("remove-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("remove-snap change failed with: %v", chg.Err()))
+
+ // ensure that the binary wrapper file got removed
+ binaryWrapper := filepath.Join(dirs.SnapBinariesDir, "foo.bar")
+ c.Assert(osutil.FileExists(binaryWrapper), Equals, false)
+
+ // data dirs
+ c.Assert(osutil.FileExists(snapInfo.DataDir()), Equals, false)
+ c.Assert(osutil.FileExists(snapInfo.CommonDataDir()), Equals, false)
+
+ // snap file and its mount
+ c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "foo_x1.snap")), Equals, false)
+ mup := systemd.MountUnitPath("/snap/foo/x1")
+ c.Assert(osutil.FileExists(mup), Equals, false)
+}
+
+func fakeSnapID(name string) string {
+ const suffix = "idididididididididididididididid"
+ return name + suffix[len(name)+1:]
+}
+
+const (
+ searchHit = `{
+ "anon_download_url": "@URL@",
+ "architecture": [
+ "all"
+ ],
+ "channel": "stable",
+ "content": "application",
+ "description": "this is a description",
+ "developer_id": "devdevdev",
+ "download_url": "@URL@",
+ "icon_url": "@ICON@",
+ "origin": "bar",
+ "package_name": "@NAME@",
+ "revision": @REVISION@,
+ "snap_id": "@SNAPID@",
+ "summary": "Foo",
+ "version": "@VERSION@"
+}`
+)
+
+var fooSnapID = fakeSnapID("foo")
+
+func (ms *mgrsSuite) prereqSnapAssertions(c *C, extraHeaders ...map[string]interface{}) *asserts.SnapDeclaration {
+ if len(extraHeaders) == 0 {
+ extraHeaders = []map[string]interface{}{{}}
+ }
+ var snapDecl *asserts.SnapDeclaration
+ for _, extraHeaders := range extraHeaders {
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-name": "foo",
+ "publisher-id": "devdevdev",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for h, v := range extraHeaders {
+ headers[h] = v
+ }
+ headers["snap-id"] = fakeSnapID(headers["snap-name"].(string))
+ a, err := ms.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = ms.storeSigning.Add(a)
+ c.Assert(err, IsNil)
+ snapDecl = a.(*asserts.SnapDeclaration)
+ }
+ return snapDecl
+}
+
+func (ms *mgrsSuite) makeStoreTestSnap(c *C, snapYaml string, revno string) (path, digest string) {
+ info, err := snap.InfoFromSnapYaml([]byte(snapYaml))
+ c.Assert(err, IsNil)
+
+ snapPath := makeTestSnap(c, snapYaml)
+
+ snapDigest, size, err := asserts.SnapFileSHA3_384(snapPath)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "snap-id": fakeSnapID(info.Name()),
+ "snap-sha3-384": snapDigest,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-revision": revno,
+ "developer-id": "devdevdev",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := ms.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = ms.storeSigning.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ return snapPath, snapDigest
+}
+
+func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
+ var baseURL string
+ fillHit := func(name string) string {
+ snapf, err := snap.Open(ms.serveSnapPath[name])
+ if err != nil {
+ panic(err)
+ }
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ if err != nil {
+ panic(err)
+ }
+ hit := strings.Replace(searchHit, "@URL@", baseURL+"/snap/"+name, -1)
+ hit = strings.Replace(hit, "@NAME@", name, -1)
+ hit = strings.Replace(hit, "@SNAPID@", fakeSnapID(name), -1)
+ hit = strings.Replace(hit, "@ICON@", baseURL+"/icon", -1)
+ hit = strings.Replace(hit, "@VERSION@", info.Version, -1)
+ hit = strings.Replace(hit, "@REVISION@", ms.serveRevision[name], -1)
+ return hit
+ }
+
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ comps := strings.Split(r.URL.Path, "/")
+ if len(comps) == 0 {
+ panic("unexpected url path: " + r.URL.Path)
+
+ }
+ switch comps[1] {
+ case "assertions":
+ ref := &asserts.Ref{
+ Type: asserts.Type(comps[2]),
+ PrimaryKey: comps[3:],
+ }
+ a, err := ref.Resolve(ms.storeSigning.Find)
+ if err == asserts.ErrNotFound {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(404)
+ w.Write([]byte(`{"status": 404}`))
+ return
+ }
+ if err != nil {
+ panic(err)
+ }
+ w.Header().Set("Content-Type", asserts.MediaType)
+ w.WriteHeader(200)
+ w.Write(asserts.Encode(a))
+ return
+ case "details":
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, fillHit(comps[2]))
+ case "metadata":
+ dec := json.NewDecoder(r.Body)
+ var input struct {
+ Snaps []struct {
+ SnapID string `json:"snap_id"`
+ Revision int `json:"revision"`
+ } `json:"snaps"`
+ }
+ err := dec.Decode(&input)
+ if err != nil {
+ panic(err)
+ }
+ var hits []json.RawMessage
+ for _, s := range input.Snaps {
+ name := ms.serveIDtoName[s.SnapID]
+ if snap.R(s.Revision) == snap.R(ms.serveRevision[name]) {
+ continue
+ }
+ hits = append(hits, json.RawMessage(fillHit(name)))
+ }
+ w.WriteHeader(http.StatusOK)
+ output, err := json.Marshal(map[string]interface{}{
+ "_embedded": map[string]interface{}{
+ "clickindex:package": hits,
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+ w.Write(output)
+ case "snap":
+ snapR, err := os.Open(ms.serveSnapPath[comps[2]])
+ if err != nil {
+ panic(err)
+ }
+ io.Copy(w, snapR)
+ default:
+ panic("unexpected url path: " + r.URL.Path)
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+
+ baseURL = mockServer.URL
+
+ detailsURL, err := url.Parse(baseURL + "/details/")
+ c.Assert(err, IsNil)
+ bulkURL, err := url.Parse(baseURL + "/metadata")
+ c.Assert(err, IsNil)
+ assertionsURL, err := url.Parse(baseURL + "/assertions/")
+ c.Assert(err, IsNil)
+ storeCfg := store.Config{
+ DetailsURI: detailsURL,
+ BulkURI: bulkURL,
+ AssertionsURI: assertionsURL,
+ }
+
+ mStore := store.New(&storeCfg, nil)
+ st := ms.o.State()
+ st.Lock()
+ snapstate.ReplaceStore(ms.o.State(), mStore)
+ st.Unlock()
+
+ return mockServer
+}
+
+func (ms *mgrsSuite) serveSnap(snapPath, revno string) {
+ snapf, err := snap.Open(snapPath)
+ if err != nil {
+ panic(err)
+ }
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ if err != nil {
+ panic(err)
+ }
+ name := info.Name()
+ ms.serveIDtoName[fakeSnapID(name)] = name
+ ms.serveSnapPath[name] = snapPath
+ ms.serveRevision[name] = revno
+}
+
+func (ms *mgrsSuite) TestHappyRemoteInstallAndUpgradeSvc(c *C) {
+ // test install through store and update, plus some mechanics
+ // of update
+ // TODO: ok to split if it gets too messy to maintain
+
+ ms.prereqSnapAssertions(c)
+
+ snapYamlContent := `name: foo
+version: @VERSION@
+apps:
+ bar:
+ command: bin/bar
+ svc:
+ command: svc
+ daemon: forking
+`
+
+ ver := "1.0"
+ revno := "42"
+ snapPath, digest := ms.makeStoreTestSnap(c, strings.Replace(snapYamlContent, "@VERSION@", ver, -1), revno)
+ ms.serveSnap(snapPath, revno)
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.Install(st, "foo", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ info, err := snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+
+ c.Check(info.Revision, Equals, snap.R(42))
+ c.Check(info.SnapID, Equals, fooSnapID)
+ c.Check(info.Version, Equals, "1.0")
+ c.Check(info.Summary(), Equals, "Foo")
+ c.Check(info.Description(), Equals, "this is a description")
+ c.Assert(osutil.FileExists(info.MountFile()), Equals, true)
+
+ pubAcct, err := assertstate.Publisher(st, info.SnapID)
+ c.Assert(err, IsNil)
+ c.Check(pubAcct.AccountID(), Equals, "devdevdev")
+ c.Check(pubAcct.Username(), Equals, "devdevdev")
+
+ snapRev42, err := assertstate.DB(st).Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": digest,
+ })
+ c.Assert(err, IsNil)
+ c.Check(snapRev42.(*asserts.SnapRevision).SnapID(), Equals, fooSnapID)
+ c.Check(snapRev42.(*asserts.SnapRevision).SnapRevision(), Equals, 42)
+
+ // check service was setup properly
+ svcFile := filepath.Join(dirs.SnapServicesDir, "snap.foo.svc.service")
+ c.Assert(osutil.FileExists(svcFile), Equals, true)
+ stat, err := os.Stat(svcFile)
+ c.Assert(err, IsNil)
+ // should _not_ be executable
+ c.Assert(stat.Mode().String(), Equals, "-rw-r--r--")
+
+ // Refresh
+
+ ver = "2.0"
+ revno = "50"
+ snapPath, digest = ms.makeStoreTestSnap(c, strings.Replace(snapYamlContent, "@VERSION@", ver, -1), revno)
+ ms.serveSnap(snapPath, revno)
+
+ ts, err = snapstate.Update(st, "foo", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg = st.NewChange("upgrade-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ info, err = snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+
+ c.Check(info.Revision, Equals, snap.R(50))
+ c.Check(info.SnapID, Equals, fooSnapID)
+ c.Check(info.Version, Equals, "2.0")
+
+ snapRev50, err := assertstate.DB(st).Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": digest,
+ })
+ c.Assert(err, IsNil)
+ c.Check(snapRev50.(*asserts.SnapRevision).SnapID(), Equals, fooSnapID)
+ c.Check(snapRev50.(*asserts.SnapRevision).SnapRevision(), Equals, 50)
+
+ // check updated wrapper
+ symlinkTarget, err := os.Readlink(info.Apps["bar"].WrapperPath())
+ c.Assert(err, IsNil)
+ c.Assert(symlinkTarget, Equals, "/usr/bin/snap")
+
+ // check updated service file
+ content, err := ioutil.ReadFile(svcFile)
+ c.Assert(err, IsNil)
+ c.Assert(strings.Contains(string(content), "/var/snap/foo/"+revno), Equals, true)
+}
+
+func (ms *mgrsSuite) TestHappyLocalInstallWithStoreMetadata(c *C) {
+ snapDecl := ms.prereqSnapAssertions(c)
+
+ snapYamlContent := `name: foo
+apps:
+ bar:
+ command: bin/bar
+`
+ snapPath := makeTestSnap(c, snapYamlContent+"version: 1.5")
+
+ si := &snap.SideInfo{
+ RealName: "foo",
+ SnapID: fooSnapID,
+ Revision: snap.R(55),
+ }
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // have the snap-declaration in the system db
+ err := assertstate.Add(st, ms.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, ms.devAcct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, snapDecl)
+ c.Assert(err, IsNil)
+
+ ts, err := snapstate.InstallPath(st, si, snapPath, "", snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ info, err := snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+ c.Check(info.Revision, Equals, snap.R(55))
+ c.Check(info.SnapID, Equals, fooSnapID)
+ c.Check(info.Version, Equals, "1.5")
+
+ // ensure that the binary wrapper file got generated with the right
+ // name
+ binaryWrapper := filepath.Join(dirs.SnapBinariesDir, "foo.bar")
+ c.Assert(osutil.IsSymlink(binaryWrapper), Equals, true)
+
+ // data dirs
+ c.Assert(osutil.IsDirectory(info.DataDir()), Equals, true)
+ c.Assert(osutil.IsDirectory(info.CommonDataDir()), Equals, true)
+
+ // snap file and its mounting
+
+ // after install the snap file is in the right dir
+ c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "foo_55.snap")), Equals, true)
+
+ // ensure the right unit is created
+ mup := systemd.MountUnitPath("/snap/foo/55")
+ content, err := ioutil.ReadFile(mup)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Matches, "(?ms).*^Where=/snap/foo/55")
+ c.Assert(string(content), Matches, "(?ms).*^What=/var/lib/snapd/snaps/foo_55.snap")
+}
+
+func (ms *mgrsSuite) TestCheckInterfaces(c *C) {
+ snapDecl := ms.prereqSnapAssertions(c)
+
+ snapYamlContent := `name: foo
+apps:
+ bar:
+ command: bin/bar
+slots:
+ network:
+`
+ snapPath := makeTestSnap(c, snapYamlContent+"version: 1.5")
+
+ si := &snap.SideInfo{
+ RealName: "foo",
+ SnapID: fooSnapID,
+ Revision: snap.R(55),
+ }
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // have the snap-declaration in the system db
+ err := assertstate.Add(st, ms.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, ms.devAcct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, snapDecl)
+ c.Assert(err, IsNil)
+
+ ts, err := snapstate.InstallPath(st, si, snapPath, "", snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), ErrorMatches, `(?s).*installation not allowed by "network" slot rule of interface "network".*`)
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+}
+
+func (ms *mgrsSuite) TestHappyRefreshControl(c *C) {
+ // test install through store and update, plus some mechanics
+ // of update
+ // TODO: ok to split if it gets too messy to maintain
+
+ ms.prereqSnapAssertions(c)
+
+ snapYamlContent := `name: foo
+version: @VERSION@
+`
+
+ ver := "1.0"
+ revno := "42"
+ snapPath, _ := ms.makeStoreTestSnap(c, strings.Replace(snapYamlContent, "@VERSION@", ver, -1), revno)
+ ms.serveSnap(snapPath, revno)
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.Install(st, "foo", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ info, err := snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+
+ c.Check(info.Revision, Equals, snap.R(42))
+
+ // Refresh
+
+ // Setup refresh control
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "bar-id",
+ "snap-name": "bar",
+ "publisher-id": "devdevdev",
+ "refresh-control": []interface{}{fooSnapID},
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDeclBar, err := ms.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = ms.storeSigning.Add(snapDeclBar)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, snapDeclBar)
+ c.Assert(err, IsNil)
+
+ snapstate.Set(st, "bar", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "bar", SnapID: "bar-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ develSigning := assertstest.NewSigningDB("devdevdev", develPrivKey)
+
+ develAccKey := assertstest.NewAccountKey(ms.storeSigning, ms.devAcct, nil, develPrivKey.PublicKey(), "")
+ err = ms.storeSigning.Add(develAccKey)
+ c.Assert(err, IsNil)
+
+ ver = "2.0"
+ revno = "50"
+ snapPath, _ = ms.makeStoreTestSnap(c, strings.Replace(snapYamlContent, "@VERSION@", ver, -1), revno)
+ ms.serveSnap(snapPath, revno)
+
+ updated, tss, err := snapstate.UpdateMany(st, []string{"foo"}, 0)
+ c.Check(updated, IsNil)
+ c.Check(tss, IsNil)
+ // no validation we, get an error
+ c.Check(err, ErrorMatches, `cannot refresh "foo" to revision 50: no validation by "bar"`)
+
+ // setup validation
+ headers = map[string]interface{}{
+ "series": "16",
+ "snap-id": "bar-id",
+ "approved-snap-id": fooSnapID,
+ "approved-snap-revision": "50",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ barValidation, err := develSigning.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = ms.storeSigning.Add(barValidation)
+ c.Assert(err, IsNil)
+
+ // ... and try again
+ updated, tss, err = snapstate.UpdateMany(st, []string{"foo"}, 0)
+ c.Assert(err, IsNil)
+ c.Assert(updated, DeepEquals, []string{"foo"})
+ c.Assert(tss, HasLen, 1)
+ chg = st.NewChange("upgrade-snaps", "...")
+ chg.AddAll(tss[0])
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ info, err = snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+
+ c.Check(info.Revision, Equals, snap.R(50))
+}
+
+// core & kernel
+
+func (ms *mgrsSuite) TestInstallCoreSnapUpdatesBootloader(c *C) {
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ defer partition.ForceBootloader(nil)
+
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ const packageOS = `
+name: core
+version: 16.04-1
+type: os
+`
+ snapPath := makeTestSnap(c, packageOS)
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.InstallPath(st, &snap.SideInfo{RealName: "core"}, snapPath, "", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ c.Assert(bootloader.BootVars, DeepEquals, map[string]string{
+ "snap_try_core": "core_x1.snap",
+ "snap_mode": "try",
+ })
+}
+
+func (ms *mgrsSuite) TestInstallKernelSnapUpdatesBootloader(c *C) {
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ defer partition.ForceBootloader(nil)
+
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ brandAcct := assertstest.NewAccount(ms.storeSigning, "my-brand", map[string]interface{}{
+ "account-id": "my-brand",
+ "verification": "certified",
+ }, "")
+ brandAccKey := assertstest.NewAccountKey(ms.storeSigning, brandAcct, nil, brandPrivKey.PublicKey(), "")
+
+ brandSigning := assertstest.NewSigningDB("my-brand", brandPrivKey)
+ model, err := brandSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "architecture": "amd64",
+ "store": "my-brand-store-id",
+ "gadget": "gadget",
+ "kernel": "krnl",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ const packageKernel = `
+name: krnl
+version: 4.0-1
+type: kernel`
+
+ files := [][]string{
+ {"kernel.img", "I'm a kernel"},
+ {"initrd.img", "...and I'm an initrd"},
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ snapPath := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // setup model assertion
+ err = assertstate.Add(st, ms.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, brandAcct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(st, brandAccKey)
+ c.Assert(err, IsNil)
+ auth.SetDevice(st, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ })
+ err = assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ ts, err := snapstate.InstallPath(st, &snap.SideInfo{RealName: "krnl"}, snapPath, "", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ c.Assert(bootloader.BootVars, DeepEquals, map[string]string{
+ "snap_try_kernel": "krnl_x1.snap",
+ "snap_mode": "try",
+ })
+}
+
+func (ms *mgrsSuite) installLocalTestSnap(c *C, snapYamlContent string) *snap.Info {
+ st := ms.o.State()
+
+ snapPath := makeTestSnap(c, snapYamlContent)
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, IsNil)
+
+ // store current state
+ snapName := info.Name()
+ var snapst snapstate.SnapState
+ snapstate.Get(st, snapName, &snapst)
+
+ ts, err := snapstate.InstallPath(st, &snap.SideInfo{RealName: snapName}, snapPath, "", snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ return info
+}
+
+func (ms *mgrsSuite) removeSnap(c *C, name string) {
+ st := ms.o.State()
+
+ ts, err := snapstate.Remove(st, name, snap.R(0))
+ c.Assert(err, IsNil)
+ chg := st.NewChange("remove-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("remove-snap change failed with: %v", chg.Err()))
+}
+
+func (ms *mgrsSuite) TestHappyRevert(c *C) {
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ x1Yaml := `name: foo
+version: 1.0
+apps:
+ x1:
+ command: bin/bar
+`
+ x1binary := filepath.Join(dirs.SnapBinariesDir, "foo.x1")
+
+ x2Yaml := `name: foo
+version: 2.0
+apps:
+ x2:
+ command: bin/bar
+`
+ x2binary := filepath.Join(dirs.SnapBinariesDir, "foo.x2")
+
+ ms.installLocalTestSnap(c, x1Yaml)
+ ms.installLocalTestSnap(c, x2Yaml)
+
+ // ensure we are on x2
+ _, err := os.Lstat(x2binary)
+ c.Assert(err, IsNil)
+ _, err = os.Lstat(x1binary)
+ c.Assert(err, ErrorMatches, ".*no such file.*")
+
+ // now do the revert
+ ts, err := snapstate.Revert(st, "foo", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("revert-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("revert-snap change failed with: %v", chg.Err()))
+
+ // ensure that we use x1 now
+ _, err = os.Lstat(x1binary)
+ c.Assert(err, IsNil)
+ _, err = os.Lstat(x2binary)
+ c.Assert(err, ErrorMatches, ".*no such file.*")
+
+ // ensure that x1,x2 is still there, revert just moves the "current"
+ // pointer
+ for _, fn := range []string{"foo_x2.snap", "foo_x1.snap"} {
+ p := filepath.Join(dirs.SnapBlobDir, fn)
+ c.Assert(osutil.FileExists(p), Equals, true)
+ }
+}
+
+func (ms *mgrsSuite) TestHappyAlias(c *C) {
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ fooYaml := `name: foo
+version: 1.0
+apps:
+ foo:
+ command: bin/foo
+ aliases: [foo_]
+ bar:
+ command: bin/bar
+ aliases: [bar,bar1]
+`
+ ms.installLocalTestSnap(c, fooYaml)
+
+ ts, err := snapstate.Alias(st, "foo", []string{"foo_", "bar", "bar1"})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("alias", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("alias change failed with: %v", chg.Err()))
+
+ foo_Alias := filepath.Join(dirs.SnapBinariesDir, "foo_")
+ dest, err := os.Readlink(foo_Alias)
+ c.Assert(err, IsNil)
+
+ c.Check(dest, Equals, "foo")
+
+ barAlias := filepath.Join(dirs.SnapBinariesDir, "bar")
+ dest, err = os.Readlink(barAlias)
+ c.Assert(err, IsNil)
+
+ c.Check(dest, Equals, "foo.bar")
+
+ bar1Alias := filepath.Join(dirs.SnapBinariesDir, "bar1")
+ dest, err = os.Readlink(bar1Alias)
+ c.Assert(err, IsNil)
+
+ c.Check(dest, Equals, "foo.bar")
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "foo_": "enabled",
+ "bar": "enabled",
+ "bar1": "enabled",
+ },
+ })
+
+ ms.removeSnap(c, "foo")
+
+ c.Check(osutil.IsSymlink(foo_Alias), Equals, false)
+ c.Check(osutil.IsSymlink(barAlias), Equals, false)
+ c.Check(osutil.IsSymlink(bar1Alias), Equals, false)
+
+ allAliases = nil
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, HasLen, 0)
+}
+
+func (ms *mgrsSuite) TestHappyUnalias(c *C) {
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ fooYaml := `name: foo
+version: 1.0
+apps:
+ foo:
+ command: bin/foo
+ aliases: [foo_]
+`
+ ms.installLocalTestSnap(c, fooYaml)
+
+ ts, err := snapstate.Alias(st, "foo", []string{"foo_"})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("alias", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("alias change failed with: %v", chg.Err()))
+
+ foo_Alias := filepath.Join(dirs.SnapBinariesDir, "foo_")
+ dest, err := os.Readlink(foo_Alias)
+ c.Assert(err, IsNil)
+
+ c.Check(dest, Equals, "foo")
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "foo_": "enabled",
+ },
+ })
+
+ ts, err = snapstate.Unalias(st, "foo", []string{"foo_"})
+ c.Assert(err, IsNil)
+ chg = st.NewChange("unalias", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("unalias change failed with: %v", chg.Err()))
+
+ c.Check(osutil.IsSymlink(foo_Alias), Equals, false)
+
+ allAliases = nil
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "foo_": "disabled",
+ },
+ })
+}
+
+func (ms *mgrsSuite) TestHappyRemoteInstallAutoAliases(c *C) {
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ "auto-aliases": []interface{}{"app1", "app2"},
+ })
+
+ snapYamlContent := `name: foo
+version: @VERSION@
+apps:
+ app1:
+ command: bin/app1
+ aliases: [app1]
+ app2:
+ command: bin/app2
+ aliases: [app2]
+`
+
+ ver := "1.0"
+ revno := "42"
+ snapPath, _ := ms.makeStoreTestSnap(c, strings.Replace(snapYamlContent, "@VERSION@", ver, -1), revno)
+ ms.serveSnap(snapPath, revno)
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.Install(st, "foo", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "app1": "auto",
+ "app2": "auto",
+ },
+ })
+
+ // check disk
+ app1Alias := filepath.Join(dirs.SnapBinariesDir, "app1")
+ dest, err := os.Readlink(app1Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "foo.app1")
+
+ app2Alias := filepath.Join(dirs.SnapBinariesDir, "app2")
+ dest, err = os.Readlink(app2Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "foo.app2")
+}
+
+func (ms *mgrsSuite) TestHappyRemoteInstallAndUpdateAutoAliases(c *C) {
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ "auto-aliases": []interface{}{"app1"},
+ })
+
+ fooYaml := `name: foo
+version: @VERSION@
+apps:
+ app1:
+ command: bin/app1
+ aliases: [app1]
+ app2:
+ command: bin/app2
+ aliases: [app2]
+`
+
+ fooPath, _ := ms.makeStoreTestSnap(c, strings.Replace(fooYaml, "@VERSION@", "1.0", -1), "10")
+ ms.serveSnap(fooPath, "10")
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.Install(st, "foo", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ info, err := snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+ c.Check(info.Revision, Equals, snap.R(10))
+ c.Check(info.Version, Equals, "1.0")
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Check(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "app1": "auto",
+ },
+ })
+ app1Alias := filepath.Join(dirs.SnapBinariesDir, "app1")
+ dest, err := os.Readlink(app1Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "foo.app1")
+
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ "auto-aliases": []interface{}{"app2"},
+ "revision": "1",
+ })
+
+ // new foo version/revision
+ fooPath, _ = ms.makeStoreTestSnap(c, strings.Replace(fooYaml, "@VERSION@", "1.5", -1), "15")
+ ms.serveSnap(fooPath, "15")
+
+ // refresh all
+ updated, tss, err := snapstate.UpdateMany(st, nil, 0)
+ c.Assert(err, IsNil)
+ c.Assert(updated, DeepEquals, []string{"foo"})
+ c.Assert(tss, HasLen, 1)
+ chg = st.NewChange("upgrade-snaps", "...")
+ chg.AddAll(tss[0])
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ info, err = snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+ c.Check(info.Revision, Equals, snap.R(15))
+ c.Check(info.Version, Equals, "1.5")
+
+ allAliases = nil
+ err = st.Get("aliases", &allAliases)
+ c.Check(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "app2": "auto",
+ },
+ })
+
+ c.Check(osutil.IsSymlink(app1Alias), Equals, false)
+
+ app2Alias := filepath.Join(dirs.SnapBinariesDir, "app2")
+ dest, err = os.Readlink(app2Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "foo.app2")
+}
+
+func (ms *mgrsSuite) TestHappyOrthogonalRefreshAutoAliases(c *C) {
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ "auto-aliases": []interface{}{"app1"},
+ }, map[string]interface{}{
+ "snap-name": "bar",
+ })
+
+ fooYaml := `name: foo
+version: @VERSION@
+apps:
+ app1:
+ command: bin/app1
+ aliases: [app1]
+ app2:
+ command: bin/app2
+ aliases: [app2]
+`
+
+ barYaml := `name: bar
+version: @VERSION@
+apps:
+ app1:
+ command: bin/app1
+ aliases: [app1]
+ app3:
+ command: bin/app3
+ aliases: [app3]
+`
+
+ fooPath, _ := ms.makeStoreTestSnap(c, strings.Replace(fooYaml, "@VERSION@", "1.0", -1), "10")
+ ms.serveSnap(fooPath, "10")
+
+ barPath, _ := ms.makeStoreTestSnap(c, strings.Replace(barYaml, "@VERSION@", "2.0", -1), "20")
+ ms.serveSnap(barPath, "20")
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ ts, err := snapstate.Install(st, "foo", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ ts, err = snapstate.Install(st, "bar", "stable", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg = st.NewChange("install-snap", "...")
+ chg.AddAll(ts)
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
+
+ info, err := snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+ c.Check(info.Revision, Equals, snap.R(10))
+ c.Check(info.Version, Equals, "1.0")
+
+ info, err = snapstate.CurrentInfo(st, "bar")
+ c.Assert(err, IsNil)
+ c.Check(info.Revision, Equals, snap.R(20))
+ c.Check(info.Version, Equals, "2.0")
+
+ var allAliases map[string]map[string]string
+ err = st.Get("aliases", &allAliases)
+ c.Check(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "app1": "auto",
+ },
+ })
+
+ // foo gets a new version/revision and a change of auto-aliases
+ // bar gets only the latter
+ // app1 is transferred from foo to bar
+ // UpdateMany after a snap-declaration refresh handles all of this
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ "auto-aliases": []interface{}{"app2"},
+ "revision": "1",
+ }, map[string]interface{}{
+ "snap-name": "bar",
+ "auto-aliases": []interface{}{"app1", "app3"},
+ "revision": "1",
+ })
+
+ // new foo version/revision
+ fooPath, _ = ms.makeStoreTestSnap(c, strings.Replace(fooYaml, "@VERSION@", "1.5", -1), "15")
+ ms.serveSnap(fooPath, "15")
+
+ // refresh all
+ err = assertstate.RefreshSnapDeclarations(st, 0)
+ c.Assert(err, IsNil)
+
+ updated, tss, err := snapstate.UpdateMany(st, nil, 0)
+ c.Assert(err, IsNil)
+ sort.Strings(updated)
+ c.Assert(updated, DeepEquals, []string{"bar", "foo"})
+ c.Assert(tss, HasLen, 3)
+ chg = st.NewChange("upgrade-snaps", "...")
+ chg.AddAll(tss[0])
+ chg.AddAll(tss[1])
+ chg.AddAll(tss[2])
+
+ st.Unlock()
+ err = ms.o.Settle()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ info, err = snapstate.CurrentInfo(st, "foo")
+ c.Assert(err, IsNil)
+ c.Check(info.Revision, Equals, snap.R(15))
+ c.Check(info.Version, Equals, "1.5")
+
+ allAliases = nil
+ err = st.Get("aliases", &allAliases)
+ c.Check(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "foo": {
+ "app2": "auto",
+ },
+ "bar": {
+ "app1": "auto",
+ "app3": "auto",
+ },
+ })
+
+ app2Alias := filepath.Join(dirs.SnapBinariesDir, "app2")
+ dest, err := os.Readlink(app2Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "foo.app2")
+
+ app1Alias := filepath.Join(dirs.SnapBinariesDir, "app1")
+ dest, err = os.Readlink(app1Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "bar.app1")
+ app3Alias := filepath.Join(dirs.SnapBinariesDir, "app3")
+ dest, err = os.Readlink(app3Alias)
+ c.Assert(err, IsNil)
+ c.Check(dest, Equals, "bar.app3")
+}
+
+type authContextSetupSuite struct {
+ o *overlord.Overlord
+ ac auth.AuthContext
+
+ storeSigning *assertstest.StoreStack
+ restoreTrusted func()
+
+ brandSigning *assertstest.SigningDB
+ deviceKey asserts.PrivateKey
+
+ model *asserts.Model
+ serial *asserts.Serial
+}
+
+func (s *authContextSetupSuite) SetUpTest(c *C) {
+ tempdir := c.MkDir()
+ dirs.SetRootDir(tempdir)
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+
+ captureAuthContext := func(_ *store.Config, ac auth.AuthContext) *store.Store {
+ s.ac = ac
+ return nil
+ }
+ r := overlord.MockStoreNew(captureAuthContext)
+ defer r()
+
+ s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey)
+ s.restoreTrusted = sysdb.InjectTrusted(s.storeSigning.Trusted)
+
+ s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey)
+
+ brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
+ "account-id": "my-brand",
+ "verification": "certified",
+ }, "")
+ s.storeSigning.Add(brandAcct)
+
+ brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, brandPrivKey.PublicKey(), "")
+ s.storeSigning.Add(brandAccKey)
+
+ model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "architecture": "amd64",
+ "store": "my-brand-store-id",
+ "gadget": "pc",
+ "kernel": "pc-kernel",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ s.model = model.(*asserts.Model)
+
+ encDevKey, err := asserts.EncodePublicKey(deviceKey.PublicKey())
+ c.Assert(err, IsNil)
+ serial, err := s.brandSigning.Sign(asserts.SerialType, map[string]interface{}{
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "serial": "7878",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": deviceKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ s.serial = serial.(*asserts.Serial)
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+ s.o = o
+
+ st := o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ prereqs := []asserts.Assertion{s.storeSigning.StoreAccountKey(""), brandAcct, brandAccKey}
+ for _, a := range prereqs {
+ err = assertstate.Add(st, a)
+ c.Assert(err, IsNil)
+ }
+}
+
+func (s *authContextSetupSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ s.restoreTrusted()
+}
+
+func (s *authContextSetupSuite) TestStoreID(c *C) {
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ st.Unlock()
+ storeID, err := s.ac.StoreID("fallback")
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Check(storeID, Equals, "fallback")
+
+ // setup model in system state
+ auth.SetDevice(st, &auth.DeviceState{
+ Brand: s.serial.BrandID(),
+ Model: s.serial.Model(),
+ Serial: s.serial.Serial(),
+ })
+ err = assertstate.Add(st, s.model)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ storeID, err = s.ac.StoreID("fallback")
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Check(storeID, Equals, "my-brand-store-id")
+}
+
+func (s *authContextSetupSuite) TestDeviceSessionRequest(c *C) {
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ st.Unlock()
+ _, _, err := s.ac.DeviceSessionRequest("NONCE")
+ st.Lock()
+ c.Check(err, Equals, auth.ErrNoSerial)
+
+ // setup serial and key in system state
+ err = assertstate.Add(st, s.serial)
+ c.Assert(err, IsNil)
+ kpMgr, err := asserts.OpenFSKeypairManager(dirs.SnapDeviceDir)
+ c.Assert(err, IsNil)
+ err = kpMgr.Put(deviceKey)
+ c.Assert(err, IsNil)
+ auth.SetDevice(st, &auth.DeviceState{
+ Brand: s.serial.BrandID(),
+ Model: s.serial.Model(),
+ Serial: s.serial.Serial(),
+ KeyID: deviceKey.PublicKey().ID(),
+ })
+
+ st.Unlock()
+ req, encSerial, err := s.ac.DeviceSessionRequest("NONCE")
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Check(bytes.HasPrefix(req, []byte("type: device-session-request\n")), Equals, true)
+ c.Check(encSerial, DeepEquals, asserts.Encode(s.serial))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package overlord implements the overall control of a snappy system.
+package overlord
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/configstate"
+ "github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/ifacestate"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/store"
+)
+
+var (
+ ensureInterval = 5 * time.Minute
+ pruneInterval = 10 * time.Minute
+ pruneWait = 24 * time.Hour * 1
+ abortWait = 24 * time.Hour * 7
+)
+
+// Overlord is the central manager of a snappy system, keeping
+// track of all available state managers and related helpers.
+type Overlord struct {
+ stateEng *StateEngine
+ // ensure loop
+ loopTomb *tomb.Tomb
+ ensureLock sync.Mutex
+ ensureTimer *time.Timer
+ ensureNext time.Time
+ pruneTimer *time.Timer
+ // restarts
+ restartHandler func(t state.RestartType)
+ // managers
+ snapMgr *snapstate.SnapManager
+ assertMgr *assertstate.AssertManager
+ ifaceMgr *ifacestate.InterfaceManager
+ hookMgr *hookstate.HookManager
+ configMgr *configstate.ConfigManager
+ deviceMgr *devicestate.DeviceManager
+}
+
+var storeNew = store.New
+
+// New creates a new Overlord with all its state managers.
+func New() (*Overlord, error) {
+ o := &Overlord{
+ loopTomb: new(tomb.Tomb),
+ }
+
+ backend := &overlordStateBackend{
+ path: dirs.SnapStateFile,
+ ensureBefore: o.ensureBefore,
+ requestRestart: o.requestRestart,
+ }
+ s, err := loadState(backend)
+ if err != nil {
+ return nil, err
+ }
+
+ o.stateEng = NewStateEngine(s)
+
+ hookMgr, err := hookstate.Manager(s)
+ if err != nil {
+ return nil, err
+ }
+ o.hookMgr = hookMgr
+ o.stateEng.AddManager(o.hookMgr)
+
+ snapMgr, err := snapstate.Manager(s)
+ if err != nil {
+ return nil, err
+ }
+ o.snapMgr = snapMgr
+ o.stateEng.AddManager(o.snapMgr)
+
+ assertMgr, err := assertstate.Manager(s)
+ if err != nil {
+ return nil, err
+ }
+ o.assertMgr = assertMgr
+ o.stateEng.AddManager(o.assertMgr)
+
+ ifaceMgr, err := ifacestate.Manager(s, hookMgr, nil)
+ if err != nil {
+ return nil, err
+ }
+ o.ifaceMgr = ifaceMgr
+ o.stateEng.AddManager(o.ifaceMgr)
+
+ configMgr, err := configstate.Manager(s, hookMgr)
+ if err != nil {
+ return nil, err
+ }
+ o.configMgr = configMgr
+
+ deviceMgr, err := devicestate.Manager(s, hookMgr)
+ if err != nil {
+ return nil, err
+ }
+ o.deviceMgr = deviceMgr
+ o.stateEng.AddManager(o.deviceMgr)
+
+ // setting up the store
+ authContext := auth.NewAuthContext(s, o.deviceMgr)
+ sto := storeNew(nil, authContext)
+ s.Lock()
+ snapstate.ReplaceStore(s, sto)
+ s.Unlock()
+
+ return o, nil
+}
+
+func loadState(backend state.Backend) (*state.State, error) {
+ if !osutil.FileExists(dirs.SnapStateFile) {
+ // fail fast, mostly interesting for tests, this dir is setup
+ // by the snapd package
+ stateDir := filepath.Dir(dirs.SnapStateFile)
+ if !osutil.IsDirectory(stateDir) {
+ return nil, fmt.Errorf("fatal: directory %q must be present", stateDir)
+ }
+ s := state.New(backend)
+ patch.Init(s)
+ return s, nil
+ }
+
+ r, err := os.Open(dirs.SnapStateFile)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read the state file: %s", err)
+ }
+ defer r.Close()
+
+ s, err := state.ReadState(backend, r)
+ if err != nil {
+ return nil, err
+ }
+
+ // one-shot migrations
+ err = patch.Apply(s)
+ if err != nil {
+ return nil, err
+ }
+ return s, nil
+}
+
+func (o *Overlord) ensureTimerSetup() {
+ o.ensureLock.Lock()
+ defer o.ensureLock.Unlock()
+ o.ensureTimer = time.NewTimer(ensureInterval)
+ o.ensureNext = time.Now().Add(ensureInterval)
+ o.pruneTimer = time.NewTimer(pruneInterval)
+}
+
+func (o *Overlord) ensureTimerReset() time.Time {
+ o.ensureLock.Lock()
+ defer o.ensureLock.Unlock()
+ now := time.Now()
+ o.ensureTimer.Reset(ensureInterval)
+ o.ensureNext = now.Add(ensureInterval)
+ return o.ensureNext
+}
+
+func (o *Overlord) ensureBefore(d time.Duration) {
+ o.ensureLock.Lock()
+ defer o.ensureLock.Unlock()
+ if o.ensureTimer == nil {
+ panic("cannot use EnsureBefore before Overlord.Loop")
+ }
+ now := time.Now()
+ next := now.Add(d)
+ if next.Before(o.ensureNext) || o.ensureNext.Before(now) {
+ o.ensureTimer.Reset(d)
+ o.ensureNext = next
+ }
+}
+
+func (o *Overlord) requestRestart(t state.RestartType) {
+ if o.restartHandler == nil {
+ logger.Noticef("restart requested but no handler set")
+ } else {
+ o.restartHandler(t)
+ }
+}
+
+// SetRestartHandler sets a handler to fulfill restart requests asynchronously.
+func (o *Overlord) SetRestartHandler(handleRestart func(t state.RestartType)) {
+ o.restartHandler = handleRestart
+}
+
+// Loop runs a loop in a goroutine to ensure the current state regularly through StateEngine Ensure.
+func (o *Overlord) Loop() {
+ o.ensureTimerSetup()
+ o.loopTomb.Go(func() error {
+ for {
+ o.ensureTimerReset()
+ // in case of errors engine logs them,
+ // continue to the next Ensure() try for now
+ o.stateEng.Ensure()
+ select {
+ case <-o.loopTomb.Dying():
+ return nil
+ case <-o.ensureTimer.C:
+ case <-o.pruneTimer.C:
+ st := o.State()
+ st.Lock()
+ st.Prune(pruneWait, abortWait)
+ st.Unlock()
+ }
+ }
+ })
+}
+
+// Stop stops the ensure loop and the managers under the StateEngine.
+func (o *Overlord) Stop() error {
+ o.loopTomb.Kill(nil)
+ err1 := o.loopTomb.Wait()
+ o.stateEng.Stop()
+ return err1
+}
+
+// Settle runs first a state engine Ensure and then wait for activities to settle.
+// That's done by waiting for all managers activities to settle while
+// making sure no immediate further Ensure is scheduled. Chiefly for tests.
+// Cannot be used in conjunction with Loop.
+func (o *Overlord) Settle() error {
+ func() {
+ o.ensureLock.Lock()
+ defer o.ensureLock.Unlock()
+ if o.ensureTimer != nil {
+ panic("cannot use Settle concurrently with other Settle or Loop calls")
+ }
+ o.ensureTimer = time.NewTimer(0)
+ }()
+
+ defer func() {
+ o.ensureLock.Lock()
+ defer o.ensureLock.Unlock()
+ o.ensureTimer.Stop()
+ o.ensureTimer = nil
+ }()
+
+ done := false
+ var errs []error
+ for !done {
+ next := o.ensureTimerReset()
+ err := o.stateEng.Ensure()
+ switch ee := err.(type) {
+ case nil:
+ case *ensureError:
+ errs = append(errs, ee.errs...)
+ default:
+ errs = append(errs, err)
+ }
+ o.stateEng.Wait()
+ o.ensureLock.Lock()
+ done = o.ensureNext.Equal(next)
+ o.ensureLock.Unlock()
+ }
+ if len(errs) != 0 {
+ return &ensureError{errs}
+ }
+ return nil
+}
+
+// State returns the system state managed by the overlord.
+func (o *Overlord) State() *state.State {
+ return o.stateEng.State()
+}
+
+// SnapManager returns the snap manager responsible for snaps under
+// the overlord.
+func (o *Overlord) SnapManager() *snapstate.SnapManager {
+ return o.snapMgr
+}
+
+// AssertManager returns the assertion manager enforcing assertions
+// under the overlord.
+func (o *Overlord) AssertManager() *assertstate.AssertManager {
+ return o.assertMgr
+}
+
+// InterfaceManager returns the interface manager maintaining
+// interface connections under the overlord.
+func (o *Overlord) InterfaceManager() *ifacestate.InterfaceManager {
+ return o.ifaceMgr
+}
+
+// HookManager returns the hook manager responsible for running hooks under the
+// overlord.
+func (o *Overlord) HookManager() *hookstate.HookManager {
+ return o.hookMgr
+}
+
+// DeviceManager returns the device manager responsible for the device identity and policies
+func (o *Overlord) DeviceManager() *devicestate.DeviceManager {
+ return o.deviceMgr
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package overlord_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "syscall"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func TestOverlord(t *testing.T) { TestingT(t) }
+
+type overlordSuite struct{}
+
+var _ = Suite(&overlordSuite{})
+
+func (ovs *overlordSuite) SetUpTest(c *C) {
+ tmpdir := c.MkDir()
+ dirs.SetRootDir(tmpdir)
+ dirs.SnapStateFile = filepath.Join(tmpdir, "test.json")
+}
+
+func (ovs *overlordSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+func (ovs *overlordSuite) TestNew(c *C) {
+ restore := patch.Mock(42, nil)
+ defer restore()
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+ c.Check(o, NotNil)
+
+ c.Check(o.SnapManager(), NotNil)
+ c.Check(o.AssertManager(), NotNil)
+ c.Check(o.InterfaceManager(), NotNil)
+ c.Check(o.DeviceManager(), NotNil)
+
+ s := o.State()
+ c.Check(s, NotNil)
+ c.Check(o.Engine().State(), Equals, s)
+
+ s.Lock()
+ defer s.Unlock()
+ var patchLevel int
+ s.Get("patch-level", &patchLevel)
+ c.Check(patchLevel, Equals, 42)
+
+ // store is setup
+ sto := snapstate.Store(s)
+ c.Check(sto, FitsTypeOf, &store.Store{})
+}
+
+func (ovs *overlordSuite) TestNewWithGoodState(c *C) {
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level))
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ state := o.State()
+ c.Assert(err, IsNil)
+ state.Lock()
+ defer state.Unlock()
+
+ d, err := state.MarshalJSON()
+ c.Assert(err, IsNil)
+
+ var got, expected map[string]interface{}
+ err = json.Unmarshal(d, &got)
+ c.Assert(err, IsNil)
+ err = json.Unmarshal(fakeState, &expected)
+ c.Assert(err, IsNil)
+
+ c.Check(got, DeepEquals, expected)
+}
+
+func (ovs *overlordSuite) TestNewWithInvalidState(c *C) {
+ fakeState := []byte(``)
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ _, err = overlord.New()
+ c.Assert(err, ErrorMatches, "EOF")
+}
+
+func (ovs *overlordSuite) TestNewWithPatches(c *C) {
+ p := func(s *state.State) error {
+ s.Set("patched", true)
+ return nil
+ }
+ patch.Mock(1, map[int]func(*state.State) error{1: p})
+
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":0}}`))
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ state := o.State()
+ c.Assert(err, IsNil)
+ state.Lock()
+ defer state.Unlock()
+
+ var level int
+ err = state.Get("patch-level", &level)
+ c.Assert(err, IsNil)
+ c.Check(level, Equals, 1)
+
+ var b bool
+ err = state.Get("patched", &b)
+ c.Assert(err, IsNil)
+ c.Check(b, Equals, true)
+}
+
+type witnessManager struct {
+ state *state.State
+ expectedEnsure int
+ ensureCalled chan struct{}
+ ensureCallback func(s *state.State) error
+}
+
+func (wm *witnessManager) Ensure() error {
+ if wm.expectedEnsure--; wm.expectedEnsure == 0 {
+ close(wm.ensureCalled)
+ return nil
+ }
+ if wm.ensureCallback != nil {
+ return wm.ensureCallback(wm.state)
+ }
+ return nil
+}
+
+func (wm *witnessManager) Stop() {
+}
+
+func (wm *witnessManager) Wait() {
+}
+
+func (ovs *overlordSuite) TestTrivialRunAndStop(c *C) {
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ o.Loop()
+
+ err = o.Stop()
+ c.Assert(err, IsNil)
+}
+
+func (ovs *overlordSuite) TestEnsureLoopRunAndStop(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(10 * time.Millisecond)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ witness := &witnessManager{
+ state: o.State(),
+ expectedEnsure: 3,
+ ensureCalled: make(chan struct{}),
+ }
+ o.Engine().AddManager(witness)
+
+ o.Loop()
+ defer o.Stop()
+
+ t0 := time.Now()
+ select {
+ case <-witness.ensureCalled:
+ case <-time.After(2 * time.Second):
+ c.Fatal("Ensure calls not happening")
+ }
+ c.Check(time.Since(t0) >= 10*time.Millisecond, Equals, true)
+
+ err = o.Stop()
+ c.Assert(err, IsNil)
+}
+
+func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBeforeImmediate(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(10 * time.Minute)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ ensure := func(s *state.State) error {
+ s.EnsureBefore(0)
+ return nil
+ }
+
+ witness := &witnessManager{
+ state: o.State(),
+ expectedEnsure: 2,
+ ensureCalled: make(chan struct{}),
+ ensureCallback: ensure,
+ }
+ se := o.Engine()
+ se.AddManager(witness)
+
+ o.Loop()
+ defer o.Stop()
+
+ select {
+ case <-witness.ensureCalled:
+ case <-time.After(2 * time.Second):
+ c.Fatal("Ensure calls not happening")
+ }
+}
+
+func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBefore(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(10 * time.Minute)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ ensure := func(s *state.State) error {
+ s.EnsureBefore(10 * time.Millisecond)
+ return nil
+ }
+
+ witness := &witnessManager{
+ state: o.State(),
+ expectedEnsure: 2,
+ ensureCalled: make(chan struct{}),
+ ensureCallback: ensure,
+ }
+ se := o.Engine()
+ se.AddManager(witness)
+
+ o.Loop()
+ defer o.Stop()
+
+ select {
+ case <-witness.ensureCalled:
+ case <-time.After(2 * time.Second):
+ c.Fatal("Ensure calls not happening")
+ }
+}
+
+func (ovs *overlordSuite) TestEnsureBeforeSleepy(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(10 * time.Minute)
+ defer restoreIntv()
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ ensure := func(s *state.State) error {
+ overlord.MockEnsureNext(o, time.Now().Add(-10*time.Hour))
+ s.EnsureBefore(0)
+ return nil
+ }
+
+ witness := &witnessManager{
+ state: o.State(),
+ expectedEnsure: 2,
+ ensureCalled: make(chan struct{}),
+ ensureCallback: ensure,
+ }
+ se := o.Engine()
+ se.AddManager(witness)
+
+ o.Loop()
+ defer o.Stop()
+
+ select {
+ case <-witness.ensureCalled:
+ case <-time.After(2 * time.Second):
+ c.Fatal("Ensure calls not happening")
+ }
+}
+
+func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBeforeOutsideEnsure(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(10 * time.Minute)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ ch := make(chan struct{})
+ ensure := func(s *state.State) error {
+ close(ch)
+ return nil
+ }
+
+ witness := &witnessManager{
+ state: o.State(),
+ expectedEnsure: 2,
+ ensureCalled: make(chan struct{}),
+ ensureCallback: ensure,
+ }
+ se := o.Engine()
+ se.AddManager(witness)
+
+ o.Loop()
+ defer o.Stop()
+
+ select {
+ case <-ch:
+ case <-time.After(2 * time.Second):
+ c.Fatal("Ensure calls not happening")
+ }
+
+ se.State().EnsureBefore(0)
+
+ select {
+ case <-witness.ensureCalled:
+ case <-time.After(2 * time.Second):
+ c.Fatal("Ensure calls not happening")
+ }
+}
+
+func (ovs *overlordSuite) TestEnsureLoopPrune(c *C) {
+ restoreIntv := overlord.MockPruneInterval(10*time.Millisecond, 5*time.Millisecond, 5*time.Millisecond)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ st := o.State()
+ st.Lock()
+ t1 := st.NewTask("foo", "...")
+ chg1 := st.NewChange("abort", "...")
+ chg1.AddTask(t1)
+ chg2 := st.NewChange("prune", "...")
+ chg2.SetStatus(state.DoneStatus)
+ st.Unlock()
+
+ o.Loop()
+ time.Sleep(50 * time.Millisecond)
+ err = o.Stop()
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ c.Assert(st.Change(chg1.ID()), Equals, chg1)
+ c.Assert(st.Change(chg2.ID()), IsNil)
+
+ c.Assert(t1.Status(), Equals, state.HoldStatus)
+}
+
+func (ovs *overlordSuite) TestCheckpoint(c *C) {
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ s := o.State()
+ s.Lock()
+ s.Set("mark", 1)
+ s.Unlock()
+
+ st, err := os.Stat(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ c.Assert(st.Mode(), Equals, os.FileMode(0600))
+
+ content, err := ioutil.ReadFile(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ c.Check(string(content), testutil.Contains, `"mark":1`)
+}
+
+type runnerManager struct {
+ runner *state.TaskRunner
+ ensureCallback func()
+}
+
+func newRunnerManager(s *state.State) *runnerManager {
+ rm := &runnerManager{
+ runner: state.NewTaskRunner(s),
+ }
+
+ rm.runner.AddHandler("runMgr1", func(t *state.Task, _ *tomb.Tomb) error {
+ s := t.State()
+ s.Lock()
+ defer s.Unlock()
+ s.Set("runMgr1Mark", 1)
+ return nil
+ }, nil)
+ rm.runner.AddHandler("runMgr2", func(t *state.Task, _ *tomb.Tomb) error {
+ s := t.State()
+ s.Lock()
+ defer s.Unlock()
+ s.Set("runMgr2Mark", 1)
+ return nil
+ }, nil)
+ rm.runner.AddHandler("runMgrEnsureBefore", func(t *state.Task, _ *tomb.Tomb) error {
+ s := t.State()
+ s.Lock()
+ defer s.Unlock()
+ s.EnsureBefore(20 * time.Millisecond)
+ return nil
+ }, nil)
+
+ return rm
+}
+
+func (rm *runnerManager) Ensure() error {
+ if rm.ensureCallback != nil {
+ rm.ensureCallback()
+ }
+ rm.runner.Ensure()
+ return nil
+}
+
+func (rm *runnerManager) Stop() {
+ rm.runner.Stop()
+}
+
+func (rm *runnerManager) Wait() {
+ rm.runner.Wait()
+}
+
+func (ovs *overlordSuite) TestTrivialSettle(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(1 * time.Minute)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ se := o.Engine()
+ s := se.State()
+ rm1 := newRunnerManager(s)
+ se.AddManager(rm1)
+
+ defer o.Engine().Stop()
+
+ s.Lock()
+ defer s.Unlock()
+
+ chg := s.NewChange("chg", "...")
+ t1 := s.NewTask("runMgr1", "1...")
+ chg.AddTask(t1)
+
+ s.Unlock()
+
+ o.Settle()
+
+ s.Lock()
+ c.Check(t1.Status(), Equals, state.DoneStatus)
+
+ var v int
+ err = s.Get("runMgr1Mark", &v)
+ c.Check(err, IsNil)
+}
+
+func (ovs *overlordSuite) TestSettleChain(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(1 * time.Minute)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ se := o.Engine()
+ s := se.State()
+ rm1 := newRunnerManager(s)
+ se.AddManager(rm1)
+
+ defer o.Engine().Stop()
+
+ s.Lock()
+ defer s.Unlock()
+
+ chg := s.NewChange("chg", "...")
+ t1 := s.NewTask("runMgr1", "1...")
+ t2 := s.NewTask("runMgr2", "2...")
+ t2.WaitFor(t1)
+ chg.AddAll(state.NewTaskSet(t1, t2))
+
+ s.Unlock()
+
+ o.Settle()
+
+ s.Lock()
+ c.Check(t1.Status(), Equals, state.DoneStatus)
+ c.Check(t2.Status(), Equals, state.DoneStatus)
+
+ var v int
+ err = s.Get("runMgr1Mark", &v)
+ c.Check(err, IsNil)
+ err = s.Get("runMgr2Mark", &v)
+ c.Check(err, IsNil)
+}
+
+func (ovs *overlordSuite) TestSettleExplicitEnsureBefore(c *C) {
+ restoreIntv := overlord.MockEnsureInterval(1 * time.Minute)
+ defer restoreIntv()
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ se := o.Engine()
+ s := se.State()
+ rm1 := newRunnerManager(s)
+ rm1.ensureCallback = func() {
+ s.Lock()
+ defer s.Unlock()
+ v := 0
+ s.Get("ensureCount", &v)
+ s.Set("ensureCount", v+1)
+ }
+
+ se.AddManager(rm1)
+
+ defer o.Engine().Stop()
+
+ s.Lock()
+ defer s.Unlock()
+
+ chg := s.NewChange("chg", "...")
+ t := s.NewTask("runMgrEnsureBefore", "...")
+ chg.AddTask(t)
+ s.Unlock()
+
+ o.Settle()
+
+ s.Lock()
+ c.Check(t.Status(), Equals, state.DoneStatus)
+
+ var v int
+ err = s.Get("ensureCount", &v)
+ c.Check(err, IsNil)
+ c.Check(v, Equals, 2)
+}
+
+func (ovs *overlordSuite) TestRequestRestartNoHandler(c *C) {
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ o.State().RequestRestart(state.RestartDaemon)
+}
+
+func (ovs *overlordSuite) TestRequestRestartHandler(c *C) {
+ o, err := overlord.New()
+ c.Assert(err, IsNil)
+
+ restartRequested := false
+
+ o.SetRestartHandler(func(t state.RestartType) {
+ restartRequested = true
+ })
+
+ o.State().RequestRestart(state.RestartDaemon)
+
+ c.Check(restartRequested, Equals, true)
+}
--- /dev/null
+package patch
+
+import (
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+// PatchesForTest returns the registered set of patches for testing purposes.
+func PatchesForTest() map[int]func(*state.State) error {
+ return patches
+}
+
+// MockPatch1ReadType replaces patch1ReadType.
+func MockPatch1ReadType(f func(name string, rev snap.Revision) (snap.Type, error)) (restore func()) {
+ old := patch1ReadType
+ patch1ReadType = f
+ return func() { patch1ReadType = old }
+}
+
+// MockLevel replaces the current implemented patch level
+func MockLevel(lv int) (restorer func()) {
+ old := Level
+ Level = lv
+ return func() { Level = old }
+}
+
+func Patch4TaskSnapSetup(task *state.Task) (*patch4SnapSetup, error) {
+ return patch4T{}.taskSnapSetup(task)
+}
+
+func Patch4StateMap(st *state.State) (map[string]patch4SnapState, error) {
+ var stateMap map[string]patch4SnapState
+ err := st.Get("snaps", &stateMap)
+
+ return stateMap, err
+}
+
+func Patch6StateMap(st *state.State) (map[string]patch6SnapState, error) {
+ var stateMap map[string]patch6SnapState
+ err := st.Get("snaps", &stateMap)
+
+ return stateMap, err
+}
+
+func Patch6SnapSetup(task *state.Task) (patch6SnapSetup, error) {
+ var snapsup patch6SnapSetup
+ err := task.Get("snap-setup", &snapsup)
+ return snapsup, err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// Level is the current implemented patch level of the state format and content.
+var Level = 6
+
+// patches maps from patch level L to the function that moves from L-1 to L.
+var patches = make(map[int]func(s *state.State) error)
+
+// Init initializes an empty state to the current implemented patch level.
+func Init(s *state.State) {
+ s.Lock()
+ defer s.Unlock()
+ if s.Get("patch-level", new(int)) != state.ErrNoState {
+ panic("internal error: expected empty state, attempting to override patch-level without actual patching")
+ }
+ s.Set("patch-level", Level)
+}
+
+// Apply applies any necessary patches to update the provided state to
+// conventions required by the current patch level of the system.
+func Apply(s *state.State) error {
+ var stateLevel int
+ s.Lock()
+ err := s.Get("patch-level", &stateLevel)
+ s.Unlock()
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ if stateLevel == Level {
+ // already at right level, nothing to do
+ return nil
+ }
+ if stateLevel > Level {
+ return fmt.Errorf("cannot downgrade: snapd is too old for the current system state (patch level %d)", stateLevel)
+ }
+
+ level := stateLevel
+ for level < Level {
+ logger.Noticef("Patching system state from level %d to %d", level, level+1)
+ patch := patches[level+1]
+ if patch == nil {
+ return fmt.Errorf("cannot upgrade: snapd is too new for the current system state (patch level %d)", level)
+ }
+ err := applyOne(patch, s, level)
+ if err != nil {
+ logger.Noticef("Cannot patch: %v", err)
+ return fmt.Errorf("cannot patch system state from level %d to %d: %v", level, level+1, err)
+ }
+ level++
+ }
+
+ return nil
+}
+
+func applyOne(patch func(s *state.State) error, s *state.State, level int) error {
+ s.Lock()
+ defer s.Unlock()
+
+ err := patch(s)
+ if err != nil {
+ return err
+ }
+
+ s.Set("patch-level", level+1)
+ return nil
+}
+
+// Mock mocks the current patch level and available patches.
+func Mock(level int, p map[int]func(*state.State) error) (restore func()) {
+ oldLevel := Level
+ oldPatches := patches
+ Level = level
+ patches = p
+ return func() {
+ Level = oldLevel
+ patches = oldPatches
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func init() {
+ patches[1] = patch1
+}
+
+type patch1SideInfo struct {
+ OfficialName string `yaml:"name,omitempty" json:"name,omitempty"`
+ SnapID string `yaml:"snap-id" json:"snap-id"`
+ Revision snap.Revision `yaml:"revision" json:"revision"`
+ Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
+ Developer string `yaml:"developer,omitempty" json:"developer,omitempty"`
+ EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
+ Size int64 `yaml:"size,omitempty" json:"size,omitempty"`
+ Sha512 string `yaml:"sha512,omitempty" json:"sha512,omitempty"`
+ Private bool `yaml:"private,omitempty" json:"private,omitempty"`
+}
+
+var patch1ReadType = func(name string, rev snap.Revision) (snap.Type, error) {
+ snapYamlFn := filepath.Join(snap.MountDir(name, rev), "meta", "snap.yaml")
+ meta, err := ioutil.ReadFile(snapYamlFn)
+ if err != nil {
+ return snap.TypeApp, err
+ }
+ info, err := snap.InfoFromSnapYaml(meta)
+ if err != nil {
+ return snap.TypeApp, err
+ }
+
+ return info.Type, nil
+}
+
+type patch1Flags int
+
+const (
+ // DevMode switches confinement to non-enforcing mode.
+ patch1DevMode = 1 << iota
+ // TryMode is set for snaps installed to try directly from a local directory.
+ patch1TryMode
+)
+
+type patch1SnapSetup struct {
+ Name string `json:"name,omitempty"`
+ Revision snap.Revision `json:"revision,omitempty"`
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+
+ Flags patch1Flags `json:"flags,omitempty"`
+
+ SnapPath string `json:"snap-path,omitempty"`
+}
+
+type patch1SnapState struct {
+ SnapType string `json:"type"`
+ Sequence []*patch1SideInfo `json:"sequence"`
+ Current snap.Revision `json:"current"`
+ Candidate *patch1SideInfo `json:"candidate,omitempty"`
+ Active bool `json:"active,omitempty"`
+ Channel string `json:"channel,omitempty"`
+ Flags patch1Flags `json:"flags,omitempty"`
+ // incremented revision used for local installs
+ LocalRevision snap.Revision `json:"local-revision,omitempty"`
+}
+
+// patch1 adds the snap type and the current revision to the snap state.
+func patch1(s *state.State) error {
+ var stateMap map[string]*patch1SnapState
+
+ err := s.Get("snaps", &stateMap)
+ if err == state.ErrNoState {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ for snapName, snapst := range stateMap {
+ seq := snapst.Sequence
+ if len(seq) == 0 {
+ continue
+ }
+ snapst.Current = seq[len(seq)-1].Revision
+ typ, err := patch1ReadType(snapName, snapst.Current)
+ if err != nil {
+ logger.Noticef("Recording type for snap %q: cannot retrieve info, assuming it's a app: %v", snapName, err)
+ } else {
+ logger.Noticef("Recording type for snap %q: setting to %q", snapName, typ)
+ }
+ snapst.SnapType = string(typ)
+ }
+
+ s.Set("snaps", stateMap)
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type patch1Suite struct{}
+
+var _ = Suite(&patch1Suite{})
+
+var statePatch1JSON = []byte(`
+{
+ "data": {
+ "patch-level": 0,
+ "snaps": {
+ "foo": {
+ "sequence": [{
+ "name": "foo1",
+ "revision": "2"
+ }, {
+ "name": "foo1",
+ "revision": "22"
+ }]
+ },
+
+ "core": {
+ "sequence": [{
+ "name": "core",
+ "revision": "1"
+ }, {
+ "name": "core",
+ "revision": "11"
+ }, {
+ "name": "core",
+ "revision": "111"
+ }]
+ },
+
+ "borken": {
+ "sequence": [{
+ "name": "borken",
+ "revision": "x1"
+ }, {
+ "name": "borken",
+ "revision": "x2"
+ }]
+ },
+
+ "wip": {
+ "candidate": {
+ "name": "wip",
+ "revision": "11"
+ }
+ }
+ }
+ }
+}
+`)
+
+func (s *patch1Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(dirs.SnapStateFile, statePatch1JSON, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *patch1Suite) TestPatch1(c *C) {
+ restore := patch.MockPatch1ReadType(s.readType)
+ defer restore()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ // go from patch-level 0 to patch-level 1
+ restorer := patch.MockLevel(1)
+ defer restorer()
+
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ expected := []struct {
+ name string
+ typ snap.Type
+ cur snap.Revision
+ }{
+ {"foo", snap.TypeApp, snap.R(22)},
+ {"core", snap.TypeOS, snap.R(111)},
+ {"borken", snap.TypeApp, snap.R(-2)},
+ {"wip", "", snap.R(0)},
+ }
+
+ for _, exp := range expected {
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, exp.name, &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snap.Type(snapst.SnapType), Equals, exp.typ)
+ c.Check(snapst.Current, Equals, exp.cur)
+ }
+
+ // ensure we only moved forward to patch-level 1
+ var patchLevel int
+ err = st.Get("patch-level", &patchLevel)
+ c.Assert(err, IsNil)
+ c.Assert(patchLevel, Equals, 1)
+}
+
+func (s *patch1Suite) readType(name string, rev snap.Revision) (snap.Type, error) {
+ if name == "borken" {
+ return snap.TypeApp, errors.New(`cannot read info for "borken" snap`)
+ }
+ // naive emulation for now, always works
+ if name == "gadget" {
+ return snap.TypeGadget, nil
+ }
+ if name == "core" {
+ return snap.TypeOS, nil
+ }
+
+ return snap.TypeApp, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func init() {
+ patches[2] = patch2
+}
+
+type patch2SideInfo struct {
+ RealName string `yaml:"name,omitempty" json:"name,omitempty"`
+ SnapID string `yaml:"snap-id" json:"snap-id"`
+ Revision snap.Revision `yaml:"revision" json:"revision"`
+ Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
+ DeveloperID string `yaml:"developer-id,omitempty" json:"developer-id,omitempty"`
+ Developer string `yaml:"developer,omitempty" json:"developer,omitempty"` // XXX: obsolete, will be retired after full backfilling of DeveloperID
+ EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
+ Size int64 `yaml:"size,omitempty" json:"size,omitempty"`
+ Sha512 string `yaml:"sha512,omitempty" json:"sha512,omitempty"`
+ Private bool `yaml:"private,omitempty" json:"private,omitempty"`
+}
+
+type patch2DownloadInfo struct {
+ AnonDownloadURL string `json:"anon-download-url,omitempty"`
+ DownloadURL string `json:"download-url,omitempty"`
+}
+
+type patch2Flags int
+
+type patch2SnapState struct {
+ SnapType string `json:"type"` // Use Type and SetType
+ Sequence []*patch2SideInfo `json:"sequence"`
+ Active bool `json:"active,omitempty"`
+ // Current indicates the current active revision if Active is
+ // true or the last active revision if Active is false
+ // (usually while a snap is being operated on or disabled)
+ Current snap.Revision `json:"current"`
+ Channel string `json:"channel,omitempty"`
+ Flags patch2Flags `json:"flags,omitempty"`
+}
+
+type patch2SnapSetup struct {
+ // FIXME: rename to RequestedChannel to convey the meaning better
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+
+ Flags patch2Flags `json:"flags,omitempty"`
+
+ SnapPath string `json:"snap-path,omitempty"`
+
+ DownloadInfo *patch2DownloadInfo `json:"download-info,omitempty"`
+ SideInfo *patch2SideInfo `json:"side-info,omitempty"`
+}
+
+func patch2SideInfoFromPatch1(oldInfo *patch1SideInfo, name string) *patch2SideInfo {
+ return &patch2SideInfo{
+ RealName: name, // NOTE: OfficialName dropped
+ SnapID: oldInfo.SnapID,
+ Revision: oldInfo.Revision,
+ Channel: oldInfo.Channel,
+ Developer: oldInfo.Developer, // NOTE: no DeveloperID in patch1SideInfo
+ EditedSummary: oldInfo.EditedSummary,
+ EditedDescription: oldInfo.EditedDescription,
+ Size: oldInfo.Size,
+ Sha512: oldInfo.Sha512,
+ Private: oldInfo.Private,
+ }
+}
+
+func patch2SequenceFromPatch1(oldSeq []*patch1SideInfo, name string) []*patch2SideInfo {
+ newSeq := make([]*patch2SideInfo, len(oldSeq))
+ for i, si := range oldSeq {
+ newSeq[i] = patch2SideInfoFromPatch1(si, name)
+ }
+
+ return newSeq
+}
+
+func patch2SnapStateFromPatch1(oldSnapState *patch1SnapState, name string) *patch2SnapState {
+ return &patch2SnapState{
+ SnapType: oldSnapState.SnapType,
+ Sequence: patch2SequenceFromPatch1(oldSnapState.Sequence, name),
+ Active: oldSnapState.Active,
+ Current: oldSnapState.Current,
+ Channel: oldSnapState.Channel,
+ Flags: patch2Flags(oldSnapState.Flags),
+ }
+}
+
+// patch2:
+// - migrates SnapSetup.Name to SnapSetup.SideInfo.RealName
+// - backfills SnapState.{Sequence,Candidate}.RealName if its missing
+func patch2(s *state.State) error {
+
+ var oldStateMap map[string]*patch1SnapState
+ err := s.Get("snaps", &oldStateMap)
+ if err == state.ErrNoState {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ newStateMap := make(map[string]*patch2SnapState, len(oldStateMap))
+
+ for key, oldSnapState := range oldStateMap {
+ newStateMap[key] = patch2SnapStateFromPatch1(oldSnapState, key)
+ }
+
+ // migrate SnapSetup in all tasks:
+ // - the new SnapSetup uses SideInfo, backfil from Candidate
+ // - also move SnapSetup.{Name,Revision} into SnapSetup.SideInfo.{RealName,Revision}
+ var oldSS patch1SnapSetup
+ for _, t := range s.Tasks() {
+ var newSS patch2SnapSetup
+ err := t.Get("snap-setup", &oldSS)
+ if err == state.ErrNoState {
+ continue
+ }
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ // some things stay the same
+ newSS.Channel = oldSS.Channel
+ newSS.Flags = patch2Flags(oldSS.Flags)
+ newSS.SnapPath = oldSS.SnapPath
+ // ... and some change
+ newSS.SideInfo = &patch2SideInfo{}
+ if snapst, ok := oldStateMap[oldSS.Name]; ok && snapst.Candidate != nil {
+ newSS.SideInfo = patch2SideInfoFromPatch1(snapst.Candidate, oldSS.Name)
+ }
+ if newSS.SideInfo.RealName == "" {
+ newSS.SideInfo.RealName = oldSS.Name
+ }
+ if newSS.SideInfo.Revision.Unset() {
+ newSS.SideInfo.Revision = oldSS.Revision
+ }
+ t.Set("snap-setup", &newSS)
+ }
+
+ s.Set("snaps", newStateMap)
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type patch2Suite struct{}
+
+var _ = Suite(&patch2Suite{})
+
+var statePatch2JSON = []byte(`
+{
+ "data": {
+ "patch-level": 1,
+ "snaps": {
+ "foo": {
+ "sequence": [{
+ "name": "",
+ "revision": "x1"
+ }, {
+ "name": "",
+ "revision": "x2"
+ }],
+ "current": "x2"
+ },
+ "bar": {
+ "candidate": {
+ "name": "",
+ "revision": "x1",
+ "snapid": "mysnapid"
+ }
+ }
+ }
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "some-change",
+ "summary": "summary-1",
+ "status": 0,
+ "task-ids": ["1"]
+ },
+ "2": {
+ "id": "2",
+ "kind": "some-other-change",
+ "summary": "summary-2",
+ "status": 0,
+ "task-ids": ["2"]
+ }
+ },
+ "tasks": {
+ "1": {
+ "id": "1",
+ "kind": "something",
+ "summary": "meep",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "name": "foo",
+ "revision": "x3"
+ }
+ },
+ "halt-tasks": [
+ "7"
+ ],
+ "change": "1"
+ },
+ "2": {
+ "id": "2",
+ "kind": "something-else",
+ "summary": "meep",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "name": "bar",
+ "revision": "26"
+ }
+ },
+ "halt-tasks": [
+ "3"
+ ],
+ "change": "2"
+ }
+ }
+}
+`)
+
+func (s *patch2Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(dirs.SnapStateFile, statePatch2JSON, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *patch2Suite) TestPatch2(c *C) {
+ restorer := patch.MockLevel(2)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ // go from patch level 1 -> 2
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ // our mocks are correct
+ c.Assert(st.Changes(), HasLen, 2)
+ c.Assert(st.Tasks(), HasLen, 2)
+
+ var snapsup snapstate.SnapSetup
+ // transition of:
+ // - SnapSetup.{Name,Revision} -> SnapSetup.SideInfo.{RealName,Revision}
+ t := st.Task("1")
+ err = t.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R("x3"),
+ })
+
+ // transition of:
+ // - SnapState.Sequence is backfilled with "RealName" (if missing)
+ var snapst snapstate.SnapState
+ err = snapstate.Get(st, "foo", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Sequence[0].RealName, Equals, "foo")
+ c.Check(snapst.Sequence[1].RealName, Equals, "foo")
+
+ // transition of:
+ // - Candidate for "bar" -> tasks SnapSetup.SideInfo
+ t = st.Task("2")
+ err = t.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "bar",
+ Revision: snap.R("x1"),
+ })
+
+ // FIXME: bar is now empty and should no longer be there?
+ err = snapstate.Get(st, "bar", &snapst)
+ c.Assert(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+func init() {
+ patches[3] = patch3
+}
+
+// patch3:
+// - migrates pending tasks and add {start,stop}-snap-services tasks
+func patch3(s *state.State) error {
+
+ // migrate all pending tasks and insert "{start,stop}-snap-server"
+ for _, t := range s.Tasks() {
+ if t.Status().Ready() {
+ continue
+ }
+
+ if t.Kind() == "link-snap" {
+ startSnapServices := s.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap services")))
+ startSnapServices.Set("snap-setup-task", t.ID())
+ startSnapServices.WaitFor(t)
+
+ chg := t.Change()
+ chg.AddTask(startSnapServices)
+ }
+
+ if t.Kind() == "unlink-snap" || t.Kind() == "unlink-current-snap" {
+ stopSnapServices := s.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap services")))
+ stopSnapServices.Set("snap-setup-task", t.ID())
+ t.WaitFor(stopSnapServices)
+
+ chg := t.Change()
+ chg.AddTask(stopSnapServices)
+ }
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type patch3Suite struct{}
+
+var _ = Suite(&patch3Suite{})
+
+var statePatch3JSON = []byte(`
+{
+ "last-task-id": 999,
+ "last-change-id": 99,
+
+ "data": {
+ "patch-level": 2
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "some-change",
+ "summary": "summary-1",
+ "status": 0,
+ "task-ids": ["1"]
+ }
+ },
+ "tasks": {
+ "1": {
+ "id": "1",
+ "kind": "link-snap",
+ "summary": "meep",
+ "status": 2,
+ "halt-tasks": [
+ "7"
+ ],
+ "change": "1"
+ },
+ "2": {
+ "id": "2",
+ "kind": "unlink-snap",
+ "summary": "meep",
+ "status": 2,
+ "halt-tasks": [
+ "3"
+ ],
+ "change": "1"
+ },
+ "3": {
+ "id": "3",
+ "kind": "unrelated",
+ "summary": "meep",
+ "status": 4,
+ "halt-tasks": [
+ "3"
+ ],
+ "change": "1"
+ },
+ "4": {
+ "id": "4",
+ "kind": "unlink-current-snap",
+ "summary": "meep",
+ "status": 2,
+ "halt-tasks": [
+ "3"
+ ],
+ "change": "1"
+ }
+ }
+}
+`)
+
+func (s *patch3Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(dirs.SnapStateFile, statePatch3JSON, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *patch3Suite) TestPatch3(c *C) {
+ restorer := patch.MockLevel(3)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ // our mocks are correct
+ st.Lock()
+ c.Assert(st.Tasks(), HasLen, 4)
+ st.Unlock()
+
+ // go from patch level 2 -> 3
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ // we got two more tasks
+ c.Assert(st.Tasks(), HasLen, 7)
+ for _, t := range st.Tasks() {
+ switch t.Kind() {
+ case "start-snap-services":
+ ht := t.WaitTasks()
+ c.Check(ht, HasLen, 1)
+ c.Check(ht[0].Kind(), Equals, "link-snap")
+ case "unlink-snap":
+ ht := t.WaitTasks()
+ c.Check(ht, HasLen, 1)
+ c.Check(ht[0].Kind(), Equals, "stop-snap-services")
+ case "unlink-current-snap":
+ ht := t.WaitTasks()
+ c.Check(ht, HasLen, 1)
+ c.Check(ht[0].Kind(), Equals, "stop-snap-services")
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func init() {
+ patches[4] = patch4
+}
+
+type patch4Flags int
+
+const (
+ patch4FlagDevMode = 1 << iota
+ patch4FlagTryMode
+ patch4FlagJailMode
+)
+
+const patch4FlagRevert = patch4Flags(0x40000000)
+
+func (f patch4Flags) DevMode() bool {
+ return f&patch4FlagDevMode != 0
+}
+
+func (f patch4Flags) TryMode() bool {
+ return f&patch4FlagTryMode != 0
+}
+
+func (f patch4Flags) JailMode() bool {
+ return f&patch4FlagJailMode != 0
+}
+
+func (f patch4Flags) Revert() bool {
+ return f&patch4FlagRevert != 0
+}
+
+type patch4DownloadInfo struct {
+ AnonDownloadURL string `json:"anon-download-url,omitempty"`
+ DownloadURL string `json:"download-url,omitempty"`
+
+ Size int64 `json:"size,omitempty"`
+ Sha3_384 string `json:"sha3-384,omitempty"`
+}
+
+type patch4SideInfo struct {
+ RealName string `yaml:"name,omitempty" json:"name,omitempty"`
+ SnapID string `yaml:"snap-id" json:"snap-id"`
+ Revision snap.Revision `yaml:"revision" json:"revision"`
+ Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
+ DeveloperID string `yaml:"developer-id,omitempty" json:"developer-id,omitempty"`
+ Developer string `yaml:"developer,omitempty" json:"developer,omitempty"`
+ EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
+ Private bool `yaml:"private,omitempty" json:"private,omitempty"`
+}
+
+type patch4SnapSetup struct {
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+ Flags patch4Flags `json:"flags,omitempty"`
+ SnapPath string `json:"snap-path,omitempty"`
+ DownloadInfo *patch4DownloadInfo `json:"download-info,omitempty"`
+ SideInfo *patch4SideInfo `json:"side-info,omitempty"`
+}
+
+func (snapsup *patch4SnapSetup) Name() string {
+ if snapsup.SideInfo.RealName == "" {
+ panic("SnapSetup.SideInfo.RealName not set")
+ }
+ return snapsup.SideInfo.RealName
+}
+
+func (snapsup *patch4SnapSetup) Revision() snap.Revision {
+ return snapsup.SideInfo.Revision
+}
+
+type patch4SnapState struct {
+ SnapType string `json:"type"` // Use Type and SetType
+ Sequence []*patch4SideInfo `json:"sequence"`
+ Active bool `json:"active,omitempty"`
+ Current snap.Revision `json:"current"`
+ Channel string `json:"channel,omitempty"`
+ Flags patch4Flags `json:"flags,omitempty"`
+}
+
+func (snapst *patch4SnapState) LastIndex(revision snap.Revision) int {
+ for i := len(snapst.Sequence) - 1; i >= 0; i-- {
+ if snapst.Sequence[i].Revision == revision {
+ return i
+ }
+ }
+ return -1
+}
+
+type patch4T struct{} // for namespacing of the helpers
+
+func (p4 patch4T) taskSnapSetup(task *state.Task) (*patch4SnapSetup, error) {
+ var snapsup patch4SnapSetup
+
+ switch err := p4.getMaybe(task, "snap-setup", &snapsup); err {
+ case state.ErrNoState:
+ // continue below
+ case nil:
+ return &snapsup, nil
+ default:
+ return nil, err
+ }
+
+ var id string
+ if err := p4.get(task, "snap-setup-task", &id); err != nil {
+ return nil, err
+ }
+
+ if err := p4.get(task.State().Task(id), "snap-setup", &snapsup); err != nil {
+ return nil, err
+ }
+
+ return &snapsup, nil
+}
+
+var errNoSnapState = errors.New("no snap state")
+
+func (p4 patch4T) snapSetupAndState(task *state.Task) (*patch4SnapSetup, *patch4SnapState, error) {
+ var snapst patch4SnapState
+
+ snapsup, err := p4.taskSnapSetup(task)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var snaps map[string]*json.RawMessage
+ err = task.State().Get("snaps", &snaps)
+ if err != nil {
+ return nil, nil, errNoSnapState
+ }
+ raw, ok := snaps[snapsup.Name()]
+ if !ok {
+ return nil, nil, errNoSnapState
+ }
+ err = json.Unmarshal([]byte(*raw), &snapst)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot get state for snap %q: %v", snapsup.Name(), err)
+ }
+
+ return snapsup, &snapst, err
+}
+
+// getMaybe calls task.Get and wraps any non-ErrNoState error in an informative message
+func (p4 patch4T) getMaybe(task *state.Task, key string, value interface{}) error {
+ return p4.gget(task, key, true, value)
+}
+
+// get calls task.Get and wraps any error in an informative message
+func (p4 patch4T) get(task *state.Task, key string, value interface{}) error {
+ return p4.gget(task, key, false, value)
+}
+
+// gget does the actual work of get and getMaybe
+func (patch4T) gget(task *state.Task, key string, passThroughMissing bool, value interface{}) error {
+ err := task.Get(key, value)
+ if err == nil || (passThroughMissing && err == state.ErrNoState) {
+ return err
+ }
+ change := task.Change()
+
+ return fmt.Errorf("cannot get %q from task %s (%s) of change %s (%s): %v",
+ key, task.ID(), task.Kind(), change.ID(), change.Kind(), err)
+}
+
+func (p4 patch4T) addCleanup(task *state.Task) error {
+ // NOTE we could check for the status of the change itself, but
+ // copy-snap-data is the one creating the trash, so if it's run there's
+ // no sense in fiddling with the change.
+ if task.Status().Ready() {
+ return nil
+ }
+
+ snapsup, err := p4.taskSnapSetup(task)
+ if err != nil {
+ return err
+ }
+
+ var tid string
+ if err := p4.get(task, "snap-setup-task", &tid); err != nil {
+ return err
+ }
+
+ change := task.Change()
+ revisionStr := ""
+ if snapsup.SideInfo != nil {
+ revisionStr = fmt.Sprintf(" (%s)", snapsup.Revision())
+ }
+
+ tasks := change.Tasks()
+ last := tasks[len(tasks)-1]
+ newTask := task.State().NewTask("cleanup", fmt.Sprintf("Clean up %q%s install", snapsup.Name(), revisionStr))
+ newTask.Set("snap-setup-task", tid)
+ newTask.WaitFor(last)
+ change.AddTask(newTask)
+
+ return nil
+}
+
+func (p4 patch4T) mangle(task *state.Task) error {
+ snapsup, snapst, err := p4.snapSetupAndState(task)
+ if err == errNoSnapState {
+ change := task.Change()
+ if change.Kind() != "install-snap" {
+ return fmt.Errorf("cannot get snap state for task %s (%s) of change %s (%s != install-snap)", task.ID(), task.Kind(), change.ID(), change.Kind())
+ }
+ // we expect pending/in-progress install changes
+ // possibly not to have reached link-sanp yet and so
+ // have no snap state yet, nothing to do
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ var hadCandidate bool
+ if err := p4.getMaybe(task, "had-candidate", &hadCandidate); err != nil && err != state.ErrNoState {
+ return err
+ }
+
+ if hadCandidate {
+ change := task.Change()
+ if change.Kind() != "revert-snap" {
+ return fmt.Errorf("had-candidate true for task %s (%s) of non-revert change %s (%s)",
+ task.ID(), task.Kind(), change.ID(), change.Kind())
+ }
+ }
+
+ task.Clear("had-candidate")
+
+ task.Set("old-candidate-index", snapst.LastIndex(snapsup.SideInfo.Revision))
+
+ return nil
+}
+
+func (p4 patch4T) addRevertFlag(task *state.Task) error {
+ var snapsup patch4SnapSetup
+ err := p4.getMaybe(task, "snap-setup", &snapsup)
+ switch err {
+ case nil:
+ snapsup.Flags |= patch4FlagRevert
+
+ // save it back
+ task.Set("snap-setup", &snapsup)
+ return nil
+ case state.ErrNoState:
+ return nil
+ default:
+ return err
+ }
+}
+
+// patch4:
+// - add Revert flag to in-progress revert-snap changes
+// - move from had-candidate to old-candidate-index in link-snap tasks
+// - add cleanup task to in-progress changes that have a copy-snap-data task
+func patch4(s *state.State) error {
+ p4 := patch4T{}
+ for _, change := range s.Changes() {
+ // change is full done, take it easy
+ if change.Status().Ready() {
+ continue
+ }
+
+ if change.Kind() != "revert-snap" {
+ continue
+ }
+ for _, task := range change.Tasks() {
+ if err := p4.addRevertFlag(task); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, task := range s.Tasks() {
+ // change is full done, take it easy
+ if task.Change().Status().Ready() {
+ continue
+ }
+
+ switch task.Kind() {
+ case "link-snap":
+ if err := p4.mangle(task); err != nil {
+ return err
+ }
+ case "copy-snap-data":
+ if err := p4.addCleanup(task); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type patch4Suite struct{}
+
+var _ = Suite(&patch4Suite{})
+
+var statePatch4JSON = []byte(`
+{
+ "last-task-id": 999,
+ "last-change-id": 99,
+
+ "data": {
+ "patch-level": 3,
+ "snaps": {
+ "a": {
+ "sequence": [
+ {"name": "", "revision": "1"},
+ {"name": "", "revision": "2"},
+ {"name": "", "revision": "3"}],
+ "current": "2"},
+ "b": {
+ "sequence": [
+ {"name": "", "revision": "1"},
+ {"name": "", "revision": "2"}],
+ "current": "2"}
+ }
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "revert-snap",
+ "summary": "revert a snap",
+ "status": 2,
+ "data": {"snap-names": ["a"]},
+ "task-ids": ["1","2","3","4"]
+ },
+ "2": {
+ "id": "2",
+ "kind": "refresh-snap",
+ "summary": "refresh b snap",
+ "status": 2,
+ "data": {"snap-names": ["b"]},
+ "task-ids": ["10","11","12","13","14","15","16"]
+ },
+ "3": {
+ "id": "3",
+ "kind": "install-snap",
+ "summary": "install c snap",
+ "status": 0,
+ "data": {"snap-names": ["c"]},
+ "task-ids": ["17", "18"]
+ }
+ },
+ "tasks": {
+ "1": {
+ "id": "1",
+ "kind": "prepare-snap",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "side-info": {"revision": "2", "name": "a"}
+ }
+ },
+ "halt-tasks": ["2"],
+ "change": "1"
+ },
+ "2": {
+ "id": "2",
+ "kind": "unlink-current-snap",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup-task": "1"
+ },
+ "wait-tasks": ["1"],
+ "halt-tasks": ["3"],
+ "change": "1"
+ },
+ "3": {
+ "id": "3",
+ "kind": "setup-profiles",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup-task": "1"
+ },
+ "wait-tasks": ["2"],
+ "halt-tasks": ["4"],
+ "change": "1"
+ },
+ "4": {
+ "id": "4",
+ "kind": "link-snap",
+ "summary": "make snap avaiblabla",
+ "status": 4,
+ "data": {
+ "had-candidate": true,
+ "snap-setup-task": "1"
+ },
+ "wait-tasks": ["3"],
+ "change": "1"
+ },
+
+ "10": {
+ "id": "10",
+ "kind": "download-snap",
+ "summary": "... download ...",
+ "status": 4,
+ "data": {"snap-setup": {"side-info": {"revision": "2", "name": "a"}}},
+ "halt-tasks": ["11"],
+ "change": "2"
+ }, "11": {
+ "id": "11",
+ "kind": "validate-snap",
+ "summary": "... check asserts...",
+ "status": 4,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["10"],
+ "halt-tasks": ["12"],
+ "change": "2"
+ }, "12": {
+ "id": "12",
+ "kind": "mount-snap",
+ "summary": "... mount...",
+ "status": 4,
+ "data": {"snap-setup-task": "10", "snap-type": "app"},
+ "wait-tasks": ["11"],
+ "halt-tasks": ["13"],
+ "change": "2"
+ }, "13": {
+ "id": "13",
+ "kind": "unlink-current-snap",
+ "summary": "... unlink...",
+ "status": 4,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["12"],
+ "halt-tasks": ["14"],
+ "change": "2"
+ }, "14": {
+ "id": "14",
+ "kind": "copy-snap-data",
+ "summary": "... copy...",
+ "status": 0,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["13"],
+ "halt-tasks": ["15"],
+ "change": "2"
+ }, "15": {
+ "id": "15",
+ "kind": "setup-profiles",
+ "summary": "... set up profile...",
+ "status": 0,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["14"],
+ "halt-tasks": ["16"],
+ "change": "2"
+ }, "16": {
+ "id": "16",
+ "kind": "link-snap",
+ "summary": "... link...",
+ "status": 0,
+ "data": {"snap-setup-task": "10", "had-candidate": false},
+ "wait-tasks": ["15"],
+ "change": "2"
+ },
+
+ "17": {
+ "id": "17",
+ "kind": "prepare-snap",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "side-info": {"revision": "1", "name": "c"}
+ }
+ },
+ "halt-tasks": ["18"],
+ "change": "1"
+ }, "18": {
+ "id": "18",
+ "kind": "link-snap",
+ "summary": "make snap avaiblabla",
+ "status": 0,
+ "data": {
+ "snap-setup-task": "17"
+ },
+ "wait-tasks": ["17"],
+ "change": "3"
+ }
+ }
+}
+`)
+
+func (s *patch4Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(dirs.SnapStateFile, statePatch4JSON, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *patch4Suite) TestPatch4OnReverts(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ // simulate that the task was running (but the change
+ // is not fully done yet)
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+ task.SetStatus(state.DoneStatus)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), IsNil)
+ c.Check(had, Equals, true)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, true)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+}
+
+func (s *patch4Suite) TestPatch4OnRevertsNoCandidateYet(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+ // its ready to run but has not run yet
+ task.Clear("had-candidate")
+ task.SetStatus(state.DoStatus)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, true)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+}
+
+func (s *patch4Suite) TestPatch4OnRefreshes(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+ // simulate that the task was running (but the change
+ // is not fully done yet)
+ task.SetStatus(state.DoneStatus)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), IsNil)
+ c.Check(had, Equals, false)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 7)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ // we added cleanup
+ c.Check(len(task.Change().Tasks()), Equals, 7+1)
+}
+
+// This test simulates a link-snap task that is scheduled but has not
+// run yet. It has no "had-candidate" data set yet.
+func (s *patch4Suite) TestPatch4OnRefreshesNoHadCandidateYet(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+ // its ready to run but has not run yet
+ task.Clear("had-candidate")
+ task.SetStatus(state.DoStatus)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 7)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+
+ snapsup, err := patch.Patch4TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ // we added cleanup
+ c.Check(len(task.Change().Tasks()), Equals, 7+1)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/wrappers"
+)
+
+func init() {
+ patches[5] = patch5
+}
+
+type log struct{}
+
+func (log) Notify(status string) {
+ logger.Noticef("patch 5: %s", status)
+}
+
+// patch5:
+// - regenerate generated .service files
+func patch5(st *state.State) error {
+ log := log{}
+
+ snapStates, err := snapstate.All(st)
+ if err != nil {
+ return err
+ }
+
+ for snapName, snapst := range snapStates {
+ if !snapst.Active {
+ continue
+ }
+
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+
+ if len(info.Apps) == 0 {
+ logger.Debugf("patch 5: skipping for %q: no apps", snapName)
+ continue
+ }
+
+ err = wrappers.StopSnapServices(info, log)
+ if err != nil {
+ return err
+ }
+
+ err = wrappers.AddSnapServices(info, log)
+ if err != nil {
+ return err
+ }
+
+ err = wrappers.StartSnapServices(info, log)
+ if err != nil {
+ return err
+ }
+
+ logger.Noticef("patch 5: %q updated", snapName)
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func init() {
+ patches[6] = patch6
+}
+
+type patch6Flags struct {
+ DevMode bool `json:"devmode,omitempty"`
+ JailMode bool `json:"jailmode,omitempty"`
+ TryMode bool `json:"trymode,omitempty"`
+ Revert bool `json:"revert,omitempty"`
+}
+
+type patch6SnapSetup struct {
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+
+ patch6Flags
+
+ SnapPath string `json:"snap-path,omitempty"`
+
+ DownloadInfo *patch4DownloadInfo `json:"download-info,omitempty"`
+ SideInfo *patch4SideInfo `json:"side-info,omitempty"`
+}
+
+type patch6SnapState struct {
+ SnapType string `json:"type"` // Use Type and SetType
+ Sequence []*patch4SideInfo `json:"sequence"`
+ Active bool `json:"active,omitempty"`
+ Current snap.Revision `json:"current"`
+ Channel string `json:"channel,omitempty"`
+ patch6Flags
+}
+
+func patch6FlagsFromPatch4(old patch4Flags) patch6Flags {
+ return patch6Flags{
+ DevMode: old.DevMode(),
+ TryMode: old.TryMode(),
+ JailMode: old.JailMode(),
+ Revert: old.Revert(),
+ }
+}
+
+// patch6:
+// - move from a flags-are-ints world to a flags-are-struct-of-bools world
+func patch6(st *state.State) error {
+ var oldStateMap map[string]*patch4SnapState
+ err := st.Get("snaps", &oldStateMap)
+ if err == state.ErrNoState {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ newStateMap := make(map[string]*patch6SnapState, len(oldStateMap))
+
+ for key, old := range oldStateMap {
+ newStateMap[key] = &patch6SnapState{
+ SnapType: old.SnapType,
+ Sequence: old.Sequence,
+ Active: old.Active,
+ Current: old.Current,
+ Channel: old.Channel,
+ patch6Flags: patch6FlagsFromPatch4(old.Flags),
+ }
+ }
+
+ for _, task := range st.Tasks() {
+ var old patch4SnapSetup
+ err := task.Get("snap-setup", &old)
+ if err == state.ErrNoState {
+ continue
+ }
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+
+ task.Set("snap-setup", &patch6SnapSetup{
+ Channel: old.Channel,
+ UserID: old.UserID,
+ SnapPath: old.SnapPath,
+ DownloadInfo: old.DownloadInfo,
+ SideInfo: old.SideInfo,
+ patch6Flags: patch6FlagsFromPatch4(old.Flags),
+ })
+ }
+
+ st.Set("snaps", newStateMap)
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type patch6Suite struct{}
+
+var _ = Suite(&patch6Suite{})
+
+var statePatch5JSON = []byte(`
+{
+ "last-task-id": 999,
+ "last-change-id": 99,
+
+ "data": {
+ "patch-level": 5,
+ "snaps": {
+ "a": {
+ "sequence": [{"name": "", "revision": "2"}],
+ "flags": 1,
+ "current": "2"},
+ "b": {
+ "sequence": [{"name": "b", "revision": "2"}],
+ "flags": 2,
+ "current": "2"},
+ "c": {
+ "sequence": [{"name": "c", "revision": "2"}],
+ "flags": 4,
+ "current": "2"}
+ }
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "install-snap",
+ "summary": "install a snap",
+ "status": 0,
+ "data": {"snap-names": ["a"]},
+ "task-ids": ["11","12"]
+ },
+ "2": {
+ "id": "2",
+ "kind": "install-snap",
+ "summary": "install b snap",
+ "status": 0,
+ "data": {"snap-names": ["b"]},
+ "task-ids": ["11","12"]
+ },
+ "3": {
+ "id": "3",
+ "kind": "revert-snap",
+ "summary": "revert c snap",
+ "status": 0,
+ "data": {"snap-names": ["c"]},
+ "task-ids": ["21","22"]
+ }
+ },
+ "tasks": {
+ "11": {
+ "id": "11",
+ "change": "1",
+ "kind": "download-snap",
+ "summary": "Download snap a from channel edge",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "edge",
+ "flags": 1
+ }},
+ "halt-tasks": ["12"]
+ },
+ "12": {"id": "12", "change": "1", "kind": "some-other-task"},
+ "21": {
+ "id": "21",
+ "change": "2",
+ "kind": "download-snap",
+ "summary": "Download snap b from channel beta",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "beta",
+ "flags": 2
+ }},
+ "halt-tasks": ["22"]
+ },
+ "22": {"id": "22", "change": "2", "kind": "some-other-task"},
+ "31": {
+ "id": "31",
+ "change": "3",
+ "kind": "prepare-snap",
+ "summary": "Prepare snap c",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "stable",
+ "flags": 1073741828
+ }},
+ "halt-tasks": ["32"]
+ },
+ "32": {"id": "32", "change": "3", "kind": "some-other-task"}
+ }
+}
+`)
+
+func (s *patch6Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(dirs.SnapStateFile, statePatch5JSON, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *patch6Suite) TestPatch6(c *C) {
+ restorer := patch.MockLevel(6)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ stateMap, err := patch.Patch4StateMap(st)
+ c.Assert(err, IsNil)
+ c.Check(int(stateMap["a"].Flags), Equals, 1)
+ c.Check(int(stateMap["b"].Flags), Equals, 2)
+ c.Check(int(stateMap["c"].Flags), Equals, 4)
+ }()
+
+ c.Assert(patch.Apply(st), IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ stateMap, err := patch.Patch6StateMap(st)
+ c.Assert(err, IsNil)
+
+ c.Check(stateMap["a"].DevMode, Equals, true)
+ c.Check(stateMap["a"].TryMode, Equals, false)
+ c.Check(stateMap["a"].JailMode, Equals, false)
+
+ c.Check(stateMap["b"].DevMode, Equals, false)
+ c.Check(stateMap["b"].TryMode, Equals, true)
+ c.Check(stateMap["b"].JailMode, Equals, false)
+
+ c.Check(stateMap["c"].DevMode, Equals, false)
+ c.Check(stateMap["c"].TryMode, Equals, false)
+ c.Check(stateMap["c"].JailMode, Equals, true)
+
+ for _, task := range st.Tasks() {
+ snapsup, err := patch.Patch6SnapSetup(task)
+ if err == state.ErrNoState {
+ continue
+ }
+ c.Assert(err, IsNil)
+
+ var snaps []string
+ c.Assert(task.Change().Get("snap-names", &snaps), IsNil)
+ c.Assert(snaps, HasLen, 1)
+
+ switch snaps[0] {
+ case "a":
+ c.Check(snapsup.DevMode, Equals, true, Commentf("a"))
+ c.Check(snapsup.TryMode, Equals, false, Commentf("a"))
+ c.Check(snapsup.JailMode, Equals, false, Commentf("a"))
+ c.Check(snapsup.Revert, Equals, false, Commentf("a"))
+ case "b":
+ c.Check(snapsup.DevMode, Equals, false, Commentf("b"))
+ c.Check(snapsup.TryMode, Equals, true, Commentf("b"))
+ c.Check(snapsup.JailMode, Equals, false, Commentf("b"))
+ c.Check(snapsup.Revert, Equals, false, Commentf("b"))
+ case "c":
+ c.Check(snapsup.DevMode, Equals, false, Commentf("c"))
+ c.Check(snapsup.TryMode, Equals, false, Commentf("c"))
+ c.Check(snapsup.JailMode, Equals, true, Commentf("c"))
+ c.Check(snapsup.Revert, Equals, true, Commentf("c"))
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "fmt"
+ "sort"
+ "testing"
+
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/state"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type patchSuite struct{}
+
+var _ = Suite(&patchSuite{})
+
+func (s *patchSuite) TestInit(c *C) {
+ restore := patch.Mock(2, nil)
+ defer restore()
+
+ st := state.New(nil)
+ patch.Init(st)
+
+ st.Lock()
+ defer st.Unlock()
+ var patchLevel int
+ err := st.Get("patch-level", &patchLevel)
+ c.Assert(err, IsNil)
+ c.Check(patchLevel, Equals, 2)
+}
+
+func (s *patchSuite) TestNothingToDo(c *C) {
+ restore := patch.Mock(2, nil)
+ defer restore()
+
+ st := state.New(nil)
+ st.Lock()
+ st.Set("patch-level", 2)
+ st.Unlock()
+ err := patch.Apply(st)
+ c.Assert(err, IsNil)
+}
+
+func (s *patchSuite) TestNoDowngrade(c *C) {
+ restore := patch.Mock(2, nil)
+ defer restore()
+
+ st := state.New(nil)
+ st.Lock()
+ st.Set("patch-level", 3)
+ st.Unlock()
+ err := patch.Apply(st)
+ c.Assert(err, ErrorMatches, `cannot downgrade: snapd is too old for the current system state \(patch level 3\)`)
+}
+
+func (s *patchSuite) TestApply(c *C) {
+ p12 := func(st *state.State) error {
+ var n int
+ st.Get("n", &n)
+ st.Set("n", n+1)
+ return nil
+ }
+ p23 := func(st *state.State) error {
+ var n int
+ st.Get("n", &n)
+ st.Set("n", n*10)
+ return nil
+ }
+ restore := patch.Mock(3, map[int]func(*state.State) error{
+ 2: p12,
+ 3: p23,
+ })
+ defer restore()
+
+ st := state.New(nil)
+ st.Lock()
+ st.Set("patch-level", 1)
+ st.Unlock()
+ err := patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ var level int
+ err = st.Get("patch-level", &level)
+ c.Assert(err, IsNil)
+ c.Check(level, Equals, 3)
+
+ var n int
+ err = st.Get("n", &n)
+ c.Assert(err, IsNil)
+ c.Check(n, Equals, 10)
+}
+
+func (s *patchSuite) TestMissing(c *C) {
+ restore := patch.Mock(3, map[int]func(*state.State) error{
+ 3: func(s *state.State) error { return nil },
+ })
+ defer restore()
+
+ st := state.New(nil)
+ st.Lock()
+ st.Set("patch-level", 1)
+ st.Unlock()
+ err := patch.Apply(st)
+ c.Assert(err, ErrorMatches, `cannot upgrade: snapd is too new for the current system state \(patch level 1\)`)
+}
+
+func (s *patchSuite) TestError(c *C) {
+ p12 := func(st *state.State) error {
+ var n int
+ st.Get("n", &n)
+ st.Set("n", n+1)
+ return nil
+ }
+ p23 := func(st *state.State) error {
+ var n int
+ st.Get("n", &n)
+ st.Set("n", n*10)
+ return fmt.Errorf("boom")
+ }
+ p34 := func(st *state.State) error {
+ var n int
+ st.Get("n", &n)
+ st.Set("n", n*100)
+ return nil
+ }
+ restore := patch.Mock(3, map[int]func(*state.State) error{
+ 2: p12,
+ 3: p23,
+ 4: p34,
+ })
+ defer restore()
+
+ st := state.New(nil)
+ st.Lock()
+ st.Set("patch-level", 1)
+ st.Unlock()
+ err := patch.Apply(st)
+ c.Assert(err, ErrorMatches, `cannot patch system state from level 2 to 3: boom`)
+
+ st.Lock()
+ defer st.Unlock()
+
+ var level int
+ err = st.Get("patch-level", &level)
+ c.Assert(err, IsNil)
+ c.Check(level, Equals, 2)
+
+ var n int
+ err = st.Get("n", &n)
+ c.Assert(err, IsNil)
+ c.Check(n, Equals, 10)
+}
+
+func (s *patchSuite) TestSanity(c *C) {
+ patches := patch.PatchesForTest()
+ levels := make([]int, 0, len(patches))
+ for l := range patches {
+ levels = append(levels, l)
+ }
+ sort.Ints(levels)
+ // all steps present
+ for i, level := range levels {
+ c.Check(level, Equals, i+1)
+ }
+ // ends at implemented patch level
+ c.Check(levels[len(levels)-1], Equals, patch.Level)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func getAliases(st *state.State, snapName string) (map[string]string, error) {
+ var allAliases map[string]*json.RawMessage
+ err := st.Get("aliases", &allAliases)
+ if err != nil {
+ return nil, err
+ }
+ raw := allAliases[snapName]
+ if raw == nil {
+ return nil, state.ErrNoState
+ }
+ var aliases map[string]string
+ err = json.Unmarshal([]byte(*raw), &aliases)
+ if err != nil {
+ return nil, fmt.Errorf("cannot unmarshal snap aliases state: %v", err)
+ }
+ return aliases, nil
+}
+
+func setAliases(st *state.State, snapName string, aliases map[string]string) {
+ var allAliases map[string]*json.RawMessage
+ err := st.Get("aliases", &allAliases)
+ if err != nil && err != state.ErrNoState {
+ panic("internal error: cannot unmarshal snap aliases state: " + err.Error())
+ }
+ if allAliases == nil {
+ allAliases = make(map[string]*json.RawMessage)
+ }
+ if len(aliases) == 0 {
+ delete(allAliases, snapName)
+ } else {
+ data, err := json.Marshal(aliases)
+ if err != nil {
+ panic("internal error: cannot marshal snap aliases state: " + err.Error())
+ }
+ raw := json.RawMessage(data)
+ allAliases[snapName] = &raw
+ }
+ st.Set("aliases", allAliases)
+}
+
+// Alias enables the provided aliases for the snap with the given name.
+func Alias(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, snapName, &snapst)
+ if err == state.ErrNoState {
+ return nil, fmt.Errorf("cannot find snap %q", snapName)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !snapst.Active {
+ return nil, fmt.Errorf("enabling aliases for disabled snap %q not supported", snapName)
+ }
+ if err := checkChangeConflict(st, snapName, nil); err != nil {
+ return nil, err
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: snapName},
+ }
+
+ alias := st.NewTask("alias", fmt.Sprintf(i18n.G("Enable aliases for snap %q"), snapsup.Name()))
+ alias.Set("snap-setup", &snapsup)
+ toEnable := map[string]string{}
+ for _, alias := range aliases {
+ toEnable[alias] = "enabled"
+ }
+ alias.Set("aliases", toEnable)
+
+ return state.NewTaskSet(alias), nil
+}
+
+// Unalias explicitly disables the provided aliases for the snap with the given name.
+func Unalias(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, snapName, &snapst)
+ if err == state.ErrNoState {
+ return nil, fmt.Errorf("cannot find snap %q", snapName)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !snapst.Active {
+ return nil, fmt.Errorf("disabling aliases for disabled snap %q not supported", snapName)
+ }
+ if err := checkChangeConflict(st, snapName, nil); err != nil {
+ return nil, err
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: snapName},
+ }
+
+ alias := st.NewTask("alias", fmt.Sprintf(i18n.G("Disable aliases for snap %q"), snapsup.Name()))
+ alias.Set("snap-setup", &snapsup)
+ toDisable := map[string]string{}
+ for _, alias := range aliases {
+ toDisable[alias] = "disabled"
+ }
+ alias.Set("aliases", toDisable)
+
+ return state.NewTaskSet(alias), nil
+}
+
+// ResetAliases resets the provided aliases for the snap with the given name to their default state, enabled for auto-aliases, disabled otherwise.
+func ResetAliases(st *state.State, snapName string, aliases []string) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, snapName, &snapst)
+ if err == state.ErrNoState {
+ return nil, fmt.Errorf("cannot find snap %q", snapName)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if err := checkChangeConflict(st, snapName, nil); err != nil {
+ return nil, err
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: snapName},
+ }
+
+ alias := st.NewTask("alias", fmt.Sprintf(i18n.G("Reset aliases for snap %q"), snapsup.Name()))
+ alias.Set("snap-setup", &snapsup)
+ toReset := map[string]string{}
+ for _, alias := range aliases {
+ toReset[alias] = "auto"
+ }
+ alias.Set("aliases", toReset)
+
+ return state.NewTaskSet(alias), nil
+}
+
+// enabledAlias returns true if status is one of the enabled alias statuses.
+func enabledAlias(status string) bool {
+ return status == "enabled" || status == "auto"
+}
+
+func (m *SnapManager) doAlias(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ var changes map[string]string
+ err = t.Get("aliases", &changes)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ autoAliases, err := AutoAliases(st, curInfo)
+ if err != nil {
+ return err
+ }
+ autoSet := make(map[string]bool, len(autoAliases))
+ for _, alias := range autoAliases {
+ autoSet[alias] = true
+ }
+
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ t.Set("old-aliases", aliasStatuses)
+ if aliasStatuses == nil {
+ aliasStatuses = make(map[string]string)
+ }
+ var add []*backend.Alias
+ var remove []*backend.Alias
+ for alias, newStatus := range changes {
+ if newStatus != "auto" && aliasStatuses[alias] == newStatus {
+ // nothing to do
+ continue
+ }
+ aliasApp := curInfo.Aliases[alias]
+ if aliasApp == nil {
+ if newStatus == "auto" {
+ // reset to default disabled status
+ delete(aliasStatuses, alias)
+ continue
+ }
+ var action string
+ switch newStatus {
+ case "enabled":
+ action = "enable"
+ case "disabled":
+ action = "disable"
+ }
+ return fmt.Errorf("cannot %s alias %q for %q, no such alias", action, alias, snapName)
+ }
+ beAlias := &backend.Alias{
+ Name: alias,
+ Target: filepath.Base(aliasApp.WrapperPath()),
+ }
+
+ if newStatus == "auto" {
+ if !autoSet[alias] {
+ newStatus = "-" // default disabled status, not stored!
+ }
+ }
+ switch newStatus {
+ case "enabled", "auto":
+ if !enabledAlias(aliasStatuses[alias]) {
+ err := checkAliasConflict(st, snapName, alias)
+ if err != nil {
+ return err
+ }
+ add = append(add, beAlias)
+ }
+ case "disabled", "-":
+ if enabledAlias(aliasStatuses[alias]) {
+ remove = append(remove, beAlias)
+ }
+ }
+ if newStatus != "-" {
+ aliasStatuses[alias] = newStatus
+ } else {
+ delete(aliasStatuses, alias)
+ }
+ }
+ if snapst.Active {
+ st.Unlock()
+ err = m.backend.UpdateAliases(add, remove)
+ st.Lock()
+ if err != nil {
+ return err
+ }
+ }
+ setAliases(st, snapName, aliasStatuses)
+ return nil
+}
+
+func (m *SnapManager) undoAlias(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ var oldStatuses map[string]string
+ err := t.Get("old-aliases", &oldStatuses)
+ if err != nil {
+ return err
+ }
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ var changes map[string]string
+ err = t.Get("aliases", &changes)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ var add []*backend.Alias
+ var remove []*backend.Alias
+Next:
+ for alias, newStatus := range changes {
+ if newStatus != "auto" && oldStatuses[alias] == newStatus {
+ // nothing to undo
+ continue
+ }
+ aliasApp := curInfo.Aliases[alias]
+ if aliasApp == nil {
+ if newStatus == "auto" {
+ // nothing to undo
+ continue
+ }
+ // unexpected
+ return fmt.Errorf("internal error: cannot re-toggle alias %q for %q, no such alias", alias, snapName)
+ }
+ beAlias := &backend.Alias{
+ Name: alias,
+ Target: filepath.Base(aliasApp.WrapperPath()),
+ }
+
+ if newStatus == "auto" {
+ if aliasStatuses[alias] != "auto" {
+ newStatus = "-" // default disabled status
+ }
+ }
+ switch newStatus {
+ case "enabled", "auto":
+ if !enabledAlias(oldStatuses[alias]) {
+ remove = append(remove, beAlias)
+
+ }
+ case "disabled", "-":
+ if enabledAlias(oldStatuses[alias]) {
+ // can actually be reinstated only if it doesn't conflict
+ err := checkAliasConflict(st, snapName, alias)
+ if err != nil {
+ if _, ok := err.(*aliasConflictError); ok {
+ // TODO mark the conflict if it was auto?
+ delete(oldStatuses, alias)
+ t.Errorf("%v", err)
+ continue Next
+ }
+ return err
+ }
+ add = append(add, beAlias)
+ }
+ }
+ }
+ if snapst.Active {
+ st.Unlock()
+ remove, err = m.backend.MatchingAliases(remove)
+ st.Lock()
+ if err != nil {
+ return fmt.Errorf("cannot list aliases for snap %q: %v", snapName, err)
+ }
+ st.Unlock()
+ add, err = m.backend.MissingAliases(add)
+ st.Lock()
+ if err != nil {
+ return fmt.Errorf("cannot list aliases for snap %q: %v", snapName, err)
+ }
+ st.Unlock()
+ err = m.backend.UpdateAliases(add, remove)
+ st.Lock()
+ if err != nil {
+ return err
+ }
+ }
+ setAliases(st, snapName, oldStatuses)
+ return nil
+
+}
+
+func (m *SnapManager) doClearAliases(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ snapsup, _, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ if len(aliasStatuses) == 0 {
+ // nothing to do
+ return nil
+ }
+ t.Set("old-aliases", aliasStatuses)
+ setAliases(st, snapName, nil)
+ return nil
+}
+
+func (m *SnapManager) undoClearAliases(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ var oldStatuses map[string]string
+ err := t.Get("old-aliases", &oldStatuses)
+ if err == state.ErrNoState {
+ // nothing to do
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ snapsup, _, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+
+ for alias, status := range oldStatuses {
+ if enabledAlias(status) {
+ // can actually be reinstated only if it doesn't conflict
+ err := checkAliasConflict(st, snapName, alias)
+ if err != nil {
+ if _, ok := err.(*aliasConflictError); ok {
+ // TODO mark the conflict if it was auto?
+ delete(oldStatuses, alias)
+ t.Errorf("%v", err)
+ continue
+ }
+ return err
+ }
+ }
+ }
+ setAliases(st, snapName, oldStatuses)
+ return nil
+}
+
+func (m *SnapManager) doSetupAliases(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ var aliases []*backend.Alias
+ for alias, aliasStatus := range aliasStatuses {
+ if enabledAlias(aliasStatus) {
+ aliasApp := curInfo.Aliases[alias]
+ if aliasApp == nil {
+ // not a known alias anymore, skip
+ continue
+ }
+ aliases = append(aliases, &backend.Alias{
+ Name: alias,
+ Target: filepath.Base(aliasApp.WrapperPath()),
+ })
+ }
+ }
+ st.Unlock()
+ defer st.Lock()
+ return m.backend.UpdateAliases(aliases, nil)
+}
+
+func (m *SnapManager) undoSetupAliases(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ var aliases []*backend.Alias
+ for alias, aliasStatus := range aliasStatuses {
+ if enabledAlias(aliasStatus) {
+ aliasApp := curInfo.Aliases[alias]
+ if aliasApp == nil {
+ // not a known alias, skip
+ continue
+ }
+ aliases = append(aliases, &backend.Alias{
+ Name: alias,
+ Target: filepath.Base(aliasApp.WrapperPath()),
+ })
+ }
+ }
+ st.Unlock()
+ rmAliases, err := m.backend.MatchingAliases(aliases)
+ st.Lock()
+ if err != nil {
+ return fmt.Errorf("cannot list aliases for snap %q: %v", snapName, err)
+ }
+ st.Unlock()
+ defer st.Lock()
+ return m.backend.UpdateAliases(nil, rmAliases)
+}
+
+func (m *SnapManager) doRemoveAliases(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ snapsup, _, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ st.Unlock()
+ defer st.Lock()
+ return m.backend.RemoveSnapAliases(snapName)
+}
+
+func checkAgainstEnabledAliases(st *state.State, checker func(alias, otherSnap string) error) error {
+ var allAliases map[string]map[string]string
+ err := st.Get("aliases", &allAliases)
+ if err == state.ErrNoState {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ for otherSnap, aliasStatuses := range allAliases {
+ for alias, aliasStatus := range aliasStatuses {
+ if enabledAlias(aliasStatus) {
+ if err := checker(alias, otherSnap); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func checkSnapAliasConflict(st *state.State, snapName string) error {
+ prefix := fmt.Sprintf("%s.", snapName)
+ return checkAgainstEnabledAliases(st, func(alias, otherSnap string) error {
+ if alias == snapName || strings.HasPrefix(alias, prefix) {
+ return fmt.Errorf("snap %q command namespace conflicts with enabled alias %q for %q", snapName, alias, otherSnap)
+ }
+ return nil
+ })
+}
+
+type aliasConflictError struct {
+ Alias string
+ Snap string
+ Reason string
+}
+
+func (e *aliasConflictError) Error() string {
+ return fmt.Sprintf("cannot enable alias %q for %q, %s", e.Alias, e.Snap, e.Reason)
+}
+
+func checkAliasConflict(st *state.State, snapName, alias string) error {
+ // check against snaps
+ var snapNames map[string]*json.RawMessage
+ err := st.Get("snaps", &snapNames)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ for name := range snapNames {
+ if name == alias || strings.HasPrefix(alias, name+".") {
+ return &aliasConflictError{
+ Alias: alias,
+ Snap: snapName,
+ Reason: fmt.Sprintf("it conflicts with the command namespace of installed snap %q", name),
+ }
+ }
+ }
+
+ // check against aliases
+ return checkAgainstEnabledAliases(st, func(otherAlias, otherSnap string) error {
+ if otherAlias == alias && otherSnap != snapName {
+ return &aliasConflictError{
+ Alias: alias,
+ Snap: snapName,
+ Reason: fmt.Sprintf("already enabled for %q", otherSnap),
+ }
+ }
+ return nil
+ })
+}
+
+// AutoAliases allows to hook support for retrieving auto-aliases of a snap.
+var AutoAliases func(st *state.State, info *snap.Info) ([]string, error)
+
+// AutoAliasesDelta compares the alias statuses with the current snap
+// declaration for the installed snaps with the given names (or all if
+// names is empty) and returns new and retired auto-aliases by snap
+// name. It accounts for already set enabled/disabled statuses but
+// does not check for conflicts.
+func AutoAliasesDelta(st *state.State, names []string) (new map[string][]string, retired map[string][]string, err error) {
+ var snapStates map[string]*SnapState
+ if len(names) == 0 {
+ var err error
+ snapStates, err = All(st)
+ if err != nil {
+ return nil, nil, err
+ }
+ } else {
+ snapStates = make(map[string]*SnapState, len(names))
+ for _, name := range names {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil {
+ return nil, nil, err
+ }
+ snapStates[name] = &snapst
+ }
+ }
+ var firstErr error
+ new = make(map[string][]string)
+ retired = make(map[string][]string)
+ for snapName, snapst := range snapStates {
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ if firstErr == nil {
+ firstErr = err
+ }
+ continue
+ }
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ if firstErr == nil {
+ firstErr = err
+ }
+ continue
+ }
+ autoAliases, err := AutoAliases(st, info)
+ if err != nil {
+ if firstErr == nil {
+ firstErr = err
+ }
+ continue
+ }
+ autoSet := make(map[string]bool, len(autoAliases))
+ for _, alias := range autoAliases {
+ autoSet[alias] = true
+ if aliasStatuses[alias] == "" { // not auto, or disabled, or enabled
+ new[snapName] = append(new[snapName], alias)
+ }
+ }
+ for alias, curStatus := range aliasStatuses {
+ if curStatus == "auto" && !autoSet[alias] {
+ retired[snapName] = append(retired[snapName], alias)
+ }
+ }
+ }
+ return new, retired, firstErr
+}
+
+func (m *SnapManager) doSetAutoAliases(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+ snapName := snapsup.Name()
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ t.Set("old-aliases", aliasStatuses)
+ if aliasStatuses == nil {
+ aliasStatuses = make(map[string]string)
+ }
+ allNew, allRetired, err := AutoAliasesDelta(st, []string{snapName})
+ if err != nil {
+ return err
+ }
+ for _, alias := range allRetired[snapName] {
+ delete(aliasStatuses, alias)
+ }
+
+ for _, alias := range allNew[snapName] {
+ aliasApp := curInfo.Aliases[alias]
+ if aliasApp == nil {
+ // not a known alias anymore or yet, skip
+ continue
+ }
+ // TODO: only mark/log conflict if this is an update instead of an install?
+ err := checkAliasConflict(st, snapName, alias)
+ if err != nil {
+ return err
+ }
+ aliasStatuses[alias] = "auto"
+ }
+ setAliases(st, snapName, aliasStatuses)
+ return nil
+}
+
+// Aliases returns a map snap -> alias -> status covering all installed snaps.
+func Aliases(st *state.State) (map[string]map[string]string, error) {
+ var snapNames map[string]*json.RawMessage
+ err := st.Get("snaps", &snapNames)
+ if err == state.ErrNoState {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ var res map[string]map[string]string
+ for snapName := range snapNames {
+ aliasStatuses, err := getAliases(st, snapName)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if len(aliasStatuses) != 0 {
+ if res == nil {
+ res = make(map[string]map[string]string)
+ }
+ res[snapName] = aliasStatuses
+ }
+ }
+ return res, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func (s *snapmgrTestSuite) TestDoSetupAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ },
+ })
+
+ t := s.state.NewTask("setup-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.DoneStatus)
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+}
+
+func (s *snapmgrTestSuite) TestDoUndoSetupAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ },
+ })
+
+ t := s.state.NewTask("setup-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.UndoneStatus)
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ {
+ op: "matching-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ {
+ op: "update-aliases",
+ rmAliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+}
+
+func (s *snapmgrTestSuite) TestAliasTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias"})
+ c.Assert(err, IsNil)
+
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "alias",
+ })
+}
+
+func (s *snapmgrTestSuite) TestDoSetupAliasesAuto(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "auto",
+ },
+ })
+
+ t := s.state.NewTask("setup-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.DoneStatus)
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+}
+
+func (s *snapmgrTestSuite) TestDoUndoSetupAliasesAuto(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "auto",
+ },
+ })
+
+ t := s.state.NewTask("setup-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.UndoneStatus)
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ {
+ op: "matching-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ {
+ op: "update-aliases",
+ rmAliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+}
+
+func (s *snapmgrTestSuite) TestAliasRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ chg := s.state.NewChange("alias", "enable an alias")
+ ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err()))
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: []*backend.Alias{{"alias1", "alias-snap.cmd1"}},
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ var allAliases map[string]map[string]string
+ err = s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {"alias1": "enabled"},
+ })
+}
+
+func (s *snapmgrTestSuite) TestUpdateAliasChangeConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("update", "...").AddAll(ts)
+
+ _, err = snapstate.Alias(s.state, "some-snap", []string{"alias1"})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestUpdateUnaliasChangeConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("update", "...").AddAll(ts)
+
+ _, err = snapstate.Unalias(s.state, "some-snap", []string{"alias1"})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestUpdateResetAliasesChangeConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("update", "...").AddAll(ts)
+
+ _, err = snapstate.ResetAliases(s.state, "some-snap", []string{"alias1"})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestAliasUpdateChangeConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias1"})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("alias", "...").AddAll(ts)
+
+ _, err = snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestAliasNoAlias(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ chg := s.state.NewChange("alias", "enable an alias")
+ ts, err := snapstate.Alias(s.state, "some-snap", []string{"alias1"})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+ c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "some-snap", no such alias.*`)
+}
+
+func (s *snapmgrTestSuite) TestAliasAliasConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ s.state.Set("aliases", map[string]map[string]string{
+ "other-snap": {"alias1": "enabled"},
+ })
+
+ chg := s.state.NewChange("alias", "enable an alias")
+ ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+ c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap".*`)
+}
+
+func (s *snapmgrTestSuite) TestAliasAutoAliasConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ s.state.Set("aliases", map[string]map[string]string{
+ "other-snap": {"alias1": "auto"},
+ })
+
+ chg := s.state.NewChange("alias", "enable an alias")
+ ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1"})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+ c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap".*`)
+}
+
+func (s *snapmgrTestSuite) TestAliasSnapCommandSpaceConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ // the command namespace of this one will conflict
+ snapstate.Set(s.state, "alias1", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias1", Revision: snap.R(3)},
+ },
+ Current: snap.R(3),
+ })
+
+ chg := s.state.NewChange("alias", "enable an alias")
+ ts, err := snapstate.Alias(s.state, "alias-snap", []string{"alias1.cmd1"})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+ c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias1.cmd1" for "alias-snap", it conflicts with the command namespace of installed snap "alias1".*`)
+}
+
+func (s *snapmgrTestSuite) TestDoClearAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {"alias1": "enabled"},
+ "other-snap": {"alias2": "enabled"},
+ })
+
+ t := s.state.NewTask("clear-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err()))
+
+ var allAliases map[string]map[string]string
+ err := s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "other-snap": {"alias2": "enabled"},
+ })
+}
+
+func (s *snapmgrTestSuite) TestDoUndoClearAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {"alias1": "enabled", "alias5": "auto"},
+ "other-snap": {"alias2": "enabled"},
+ })
+
+ t := s.state.NewTask("clear-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err()))
+
+ var allAliases map[string]map[string]string
+ err := s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {"alias1": "enabled", "alias5": "auto"},
+ "other-snap": {"alias2": "enabled"},
+ })
+}
+
+func (s *snapmgrTestSuite) TestDoUndoClearAliasesConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias5": "auto",
+ "alias9": "enabled",
+ "alias10": "auto",
+ },
+ "other-snap": {"alias2": "enabled"},
+ })
+
+ grabAlias9_10 := func(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var allAliases map[string]map[string]string
+ err := st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Assert(allAliases, DeepEquals, map[string]map[string]string{
+ "other-snap": {"alias2": "enabled"},
+ })
+
+ st.Set("aliases", map[string]map[string]string{
+ "other-snap": {
+ "alias2": "enabled",
+ "alias9": "enabled",
+ "alias10": "enabled",
+ },
+ })
+ return nil
+ }
+
+ s.snapmgr.AddAdhocTaskHandler("grab-alias9_10", grabAlias9_10, nil)
+
+ t := s.state.NewTask("clear-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ tgrab9_10 := s.state.NewTask("grab-alias9_10", "grab alias9&alias10 for other-snap")
+ tgrab9_10.WaitFor(t)
+ chg.AddTask(tgrab9_10)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(tgrab9_10)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 5; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err()))
+
+ var allAliases map[string]map[string]string
+ err := s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias5": "auto",
+ },
+ "other-snap": {
+ "alias2": "enabled",
+ "alias9": "enabled",
+ "alias10": "enabled",
+ },
+ })
+
+ c.Check(t.Log(), HasLen, 2)
+ c.Check(t.Log()[0]+t.Log()[1], Matches, `.* ERROR cannot enable alias "alias9" for "alias-snap", already enabled for "other-snap".*`)
+}
+
+var statusesMatrix = []struct {
+ alias string
+ beforeStatus string
+ action string
+ status string
+ mutation string
+}{
+ {"alias1", "", "alias", "enabled", "add"},
+ {"alias1", "enabled", "alias", "enabled", "-"},
+ {"alias1", "disabled", "alias", "enabled", "add"},
+ {"alias1", "auto", "alias", "enabled", "-"},
+ {"alias1", "", "unalias", "disabled", "-"},
+ {"alias1", "enabled", "unalias", "disabled", "rm"},
+ {"alias1", "disabled", "unalias", "disabled", "-"},
+ {"alias1", "auto", "unalias", "disabled", "rm"},
+ {"alias1", "", "reset", "", "-"},
+ {"alias1", "enabled", "reset", "", "rm"},
+ {"alias1", "disabled", "reset", "", "-"},
+ {"alias1", "auto", "reset", "", "rm"}, // used to retire auto-aliases
+ {"alias5", "", "reset", "auto", "add"},
+ {"alias5", "enabled", "reset", "auto", "-"},
+ {"alias5", "disabled", "reset", "auto", "add"},
+ {"alias5", "auto", "reset", "auto", "-"},
+ {"alias1gone", "", "reset", "", "-"},
+ {"alias1gone", "enabled", "reset", "", "-"},
+ {"alias1gone", "disabled", "reset", "", "-"},
+ {"alias1gone", "auto", "reset", "", "-"},
+ {"alias5gone", "", "reset", "", "-"},
+ {"alias5gone", "enabled", "reset", "", "-"},
+ {"alias5gone", "disabled", "reset", "", "-"},
+ {"alias5gone", "auto", "reset", "", "-"},
+}
+
+func (s *snapmgrTestSuite) TestAliasMatrixRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ // alias1 is a non auto-alias
+ // alias5 is an auto-alias
+ // alias1gone is a non auto-alias and doesn't have an entry in the current snap revision anymore
+ // alias5gone is an auto-alias and doesn't have an entry in the current snap revision anymore
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias5", "alias5gone"}, nil
+ }
+ cmds := map[string]string{
+ "alias1": "cmd1",
+ "alias5": "cmd5",
+ }
+
+ defer s.snapmgr.Stop()
+ for _, scenario := range statusesMatrix {
+ scenAlias := scenario.alias
+ if scenario.beforeStatus != "" {
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ scenAlias: scenario.beforeStatus,
+ },
+ })
+ } else {
+ s.state.Set("aliases", nil)
+ }
+
+ chg := s.state.NewChange("scenario", "...")
+ var err error
+ var ts *state.TaskSet
+ targets := []string{scenAlias}
+ switch scenario.action {
+ case "alias":
+ ts, err = snapstate.Alias(s.state, "alias-snap", targets)
+ case "unalias":
+ ts, err = snapstate.Unalias(s.state, "alias-snap", targets)
+ case "reset":
+ ts, err = snapstate.ResetAliases(s.state, "alias-snap", targets)
+ }
+ c.Assert(err, IsNil)
+
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%#v: %v", scenario, chg.Err()))
+ var aliases []*backend.Alias
+ var rmAliases []*backend.Alias
+ beAlias := &backend.Alias{Name: scenAlias, Target: fmt.Sprintf("alias-snap.%s", cmds[scenAlias])}
+ switch scenario.mutation {
+ case "-":
+ case "add":
+ aliases = []*backend.Alias{beAlias}
+ case "rm":
+ rmAliases = []*backend.Alias{beAlias}
+ }
+
+ comm := Commentf("%#v", scenario)
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: aliases,
+ rmAliases: rmAliases,
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops(), comm)
+ c.Check(s.fakeBackend.ops, DeepEquals, expected, comm)
+
+ var allAliases map[string]map[string]string
+ err = s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ if scenario.status != "" {
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {scenAlias: scenario.status},
+ }, comm)
+ } else {
+ c.Check(allAliases, HasLen, 0, comm)
+ }
+
+ s.fakeBackend.ops = nil
+ }
+}
+
+func (s *snapmgrTestSuite) TestAliasMatrixTotalUndoRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ // alias1 is a non auto-alias
+ // alias5 is an auto-alias
+ // alias1gone is a non auto-alias and doesn't have an entry in the snap anymore
+ // alias5gone is an auto-alias and doesn't have an entry in the snap any
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias5", "alias5gone"}, nil
+ }
+ cmds := map[string]string{
+ "alias1": "cmd1",
+ "alias5": "cmd5",
+ }
+
+ defer s.snapmgr.Stop()
+ for _, scenario := range statusesMatrix {
+ scenAlias := scenario.alias
+ if scenario.beforeStatus != "" {
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ scenAlias: scenario.beforeStatus,
+ },
+ })
+ } else {
+ s.state.Set("aliases", nil)
+ }
+
+ chg := s.state.NewChange("scenario", "...")
+ var err error
+ var ts *state.TaskSet
+ targets := []string{scenAlias}
+
+ switch scenario.action {
+ case "alias":
+ ts, err = snapstate.Alias(s.state, "alias-snap", targets)
+ case "unalias":
+ ts, err = snapstate.Unalias(s.state, "alias-snap", targets)
+ case "reset":
+ ts, err = snapstate.ResetAliases(s.state, "alias-snap", targets)
+ }
+ c.Assert(err, IsNil)
+
+ chg.AddAll(ts)
+
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+ s.state.Lock()
+
+ c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("%#v: %v", scenario, chg.Err()))
+ var aliases []*backend.Alias
+ var rmAliases []*backend.Alias
+ beAlias := &backend.Alias{Name: scenAlias, Target: fmt.Sprintf("alias-snap.%s", cmds[scenAlias])}
+ switch scenario.mutation {
+ case "-":
+ case "add":
+ aliases = []*backend.Alias{beAlias}
+ case "rm":
+ rmAliases = []*backend.Alias{beAlias}
+ }
+
+ comm := Commentf("%#v", scenario)
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ aliases: aliases,
+ rmAliases: rmAliases,
+ },
+ {
+ op: "matching-aliases",
+ aliases: aliases,
+ },
+ {
+ op: "missing-aliases",
+ aliases: rmAliases,
+ },
+ {
+ op: "update-aliases",
+ aliases: rmAliases,
+ rmAliases: aliases,
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops(), comm)
+ c.Check(s.fakeBackend.ops, DeepEquals, expected, comm)
+
+ var allAliases map[string]map[string]string
+ err = s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ if scenario.beforeStatus != "" {
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {scenAlias: scenario.beforeStatus},
+ }, comm)
+ } else {
+ c.Check(allAliases, HasLen, 0, comm)
+ }
+
+ s.fakeBackend.ops = nil
+ }
+}
+
+func (s *snapmgrTestSuite) TestDisabledSnapResetAliasesRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: false,
+ })
+
+ // alias1 is a non auto-alias
+ // alias5 is an auto-alias
+ // alias1gone is a non auto-alias and doesn't have an entry in the current snap revision anymore
+ // alias5gone is an auto-alias and doesn't have an entry in the current snap revision anymore
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias5", "alias5gone"}, nil
+ }
+
+ defer s.snapmgr.Stop()
+ for _, scenario := range statusesMatrix {
+ if scenario.action != "reset" {
+ // we reuse the scenarios but here want to test only reset i.e. ResetAliases for the disabled snap case (the other actions are still unsupported for disabled snaps)
+ continue
+ }
+
+ scenAlias := scenario.alias
+ if scenario.beforeStatus != "" {
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ scenAlias: scenario.beforeStatus,
+ },
+ })
+ } else {
+ s.state.Set("aliases", nil)
+ }
+
+ chg := s.state.NewChange("scenario", "...")
+ var err error
+ var ts *state.TaskSet
+ targets := []string{scenAlias}
+ ts, err = snapstate.ResetAliases(s.state, "alias-snap", targets)
+ c.Assert(err, IsNil)
+
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%#v: %v", scenario, chg.Err()))
+
+ comm := Commentf("%#v", scenario)
+ // no mutation
+ c.Check(s.fakeBackend.ops, HasLen, 0, comm)
+
+ var allAliases map[string]map[string]string
+ err = s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ if scenario.status != "" {
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {scenAlias: scenario.status},
+ }, comm)
+ } else {
+ c.Check(allAliases, HasLen, 0, comm)
+ }
+
+ s.fakeBackend.ops = nil
+ }
+}
+
+func (s *snapmgrTestSuite) TestDisabledSnapResetAliasesTotalUndoRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: false,
+ })
+
+ // alias1 is a non auto-alias
+ // alias5 is an auto-alias
+ // alias1gone is a non auto-alias and doesn't have an entry in the snap anymore
+ // alias5gone is an auto-alias and doesn't have an entry in the snap any
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias5", "alias5gone"}, nil
+ }
+
+ defer s.snapmgr.Stop()
+ for _, scenario := range statusesMatrix {
+ if scenario.action != "reset" {
+ // we reuse the scenarios but here want to test only reset i.e. ResetAliases for the disabled snap case (the other actions are still unsupported for disabled snaps)
+ continue
+ }
+
+ scenAlias := scenario.alias
+ if scenario.beforeStatus != "" {
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ scenAlias: scenario.beforeStatus,
+ },
+ })
+ } else {
+ s.state.Set("aliases", nil)
+ }
+
+ chg := s.state.NewChange("scenario", "...")
+ var err error
+ var ts *state.TaskSet
+ targets := []string{scenAlias}
+
+ ts, err = snapstate.ResetAliases(s.state, "alias-snap", targets)
+ c.Assert(err, IsNil)
+
+ chg.AddAll(ts)
+
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+ s.state.Lock()
+
+ c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("%#v: %v", scenario, chg.Err()))
+
+ comm := Commentf("%#v", scenario)
+ // no mutation
+ c.Check(s.fakeBackend.ops, HasLen, 0, comm)
+
+ var allAliases map[string]map[string]string
+ err = s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ if scenario.beforeStatus != "" {
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {scenAlias: scenario.beforeStatus},
+ }, comm)
+ } else {
+ c.Check(allAliases, HasLen, 0, comm)
+ }
+
+ s.fakeBackend.ops = nil
+ }
+}
+
+func (s *snapmgrTestSuite) TestUnliasTotalUndoRunThroughAliasConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ defer s.snapmgr.Stop()
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ },
+ })
+
+ chg := s.state.NewChange("scenario", "...")
+ ts, err := snapstate.Unalias(s.state, "alias-snap", []string{"alias1"})
+ c.Assert(err, IsNil)
+
+ chg.AddAll(ts)
+
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+
+ grabAlias1 := func(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var allAliases map[string]map[string]string
+ err := st.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Assert(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "disabled",
+ },
+ })
+
+ st.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "disabled",
+ },
+ "other-snap": {
+ "alias1": "enabled",
+ },
+ })
+ return nil
+ }
+
+ s.snapmgr.AddAdhocTaskHandler("grab-alias1", grabAlias1, nil)
+
+ tgrab1 := s.state.NewTask("grab-alias1", "grab alias1 for other-snap")
+ tgrab1.WaitFor(last)
+ chg.AddTask(tgrab1)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(tgrab1)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 5; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("%v", chg.Err()))
+ rmAliases := []*backend.Alias{{"alias1", "alias-snap.cmd1"}}
+
+ expected := fakeOps{
+ {
+ op: "update-aliases",
+ rmAliases: rmAliases,
+ },
+ {
+ op: "matching-aliases",
+ },
+ {
+ op: "missing-aliases",
+ },
+ {
+ op: "update-aliases",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ var allAliases map[string]map[string]string
+ err = s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "other-snap": {
+ "alias1": "enabled",
+ },
+ })
+
+ c.Check(last.Log(), HasLen, 1)
+ c.Check(last.Log()[0], Matches, `.* ERROR cannot enable alias "alias1" for "alias-snap", already enabled for "other-snap"`)
+
+}
+
+func (s *snapmgrTestSuite) TestAutoAliasesDelta(c *C) {
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias1", "alias2", "alias4", "alias5"}, nil
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias2": "disabled",
+ "alias3": "auto",
+ },
+ })
+
+ new, retired, err := snapstate.AutoAliasesDelta(s.state, []string{"alias-snap"})
+ c.Assert(err, IsNil)
+
+ c.Check(new, DeepEquals, map[string][]string{
+ "alias-snap": {"alias4", "alias5"},
+ })
+
+ c.Check(retired, DeepEquals, map[string][]string{
+ "alias-snap": {"alias3"},
+ })
+}
+
+func (s *snapmgrTestSuite) TestAutoAliasesDeltaAll(c *C) {
+ seen := make(map[string]bool)
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ seen[info.Name()] = true
+ if info.Name() == "alias-snap" {
+ return []string{"alias1", "alias2", "alias4", "alias5"}, nil
+ }
+ return nil, nil
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+ snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "other-snap", Revision: snap.R(2)},
+ },
+ Current: snap.R(2),
+ Active: true,
+ })
+
+ new, retired, err := snapstate.AutoAliasesDelta(s.state, nil)
+ c.Assert(err, IsNil)
+
+ c.Check(new, DeepEquals, map[string][]string{
+ "alias-snap": {"alias1", "alias2", "alias4", "alias5"},
+ })
+
+ c.Check(retired, HasLen, 0)
+
+ c.Check(seen, DeepEquals, map[string]bool{
+ "alias-snap": true,
+ "other-snap": true,
+ })
+}
+
+func (s *snapmgrTestSuite) TestDoSetAutoAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias1", "alias2", "alias4", "alias5"}, nil
+ }
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias2": "auto",
+ "alias3": "auto",
+ "alias5": "disabled",
+ },
+ })
+
+ t := s.state.NewTask("set-auto-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err()))
+
+ var allAliases map[string]map[string]string
+ err := s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias2": "auto",
+ "alias4": "auto",
+ "alias5": "disabled",
+ },
+ })
+}
+
+func (s *snapmgrTestSuite) TestDoUndoSetAutoAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias1", "alias2", "alias4", "alias5"}, nil
+ }
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias2": "auto",
+ "alias3": "auto",
+ "alias5": "disabled",
+ },
+ })
+
+ t := s.state.NewTask("set-auto-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.UndoneStatus, Commentf("%v", chg.Err()))
+
+ var allAliases map[string]map[string]string
+ err := s.state.Get("aliases", &allAliases)
+ c.Assert(err, IsNil)
+ c.Check(allAliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias2": "auto",
+ "alias3": "auto",
+ "alias5": "disabled",
+ },
+ })
+}
+
+func (s *snapmgrTestSuite) TestDoSetAutoAliasesConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ c.Check(info.Name(), Equals, "alias-snap")
+ return []string{"alias1", "alias2", "alias4", "alias5"}, nil
+ }
+
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias3": "auto",
+ "alias5": "disabled",
+ },
+ "other-snap": {
+ "alias4": "enabled",
+ },
+ })
+
+ t := s.state.NewTask("set-auto-aliases", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "alias-snap"},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ c.Check(t.Status(), Equals, state.ErrorStatus, Commentf("%v", chg.Err()))
+ c.Check(chg.Err(), ErrorMatches, `(?s).*cannot enable alias "alias4" for "alias-snap", already enabled for "other-snap".*`)
+}
+
+func (s *snapmgrTestSuite) TestAliases(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // nothing
+ aliases, err := snapstate.Aliases(s.state)
+ c.Assert(err, IsNil)
+ c.Check(aliases, HasLen, 0)
+
+ // snaps with aliases
+ snapstate.Set(s.state, "alias-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ snapstate.Set(s.state, "alias-snap2", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "alias-snap2", Revision: snap.R(12)},
+ },
+ Current: snap.R(12),
+ Active: true,
+ })
+
+ snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "other-snap", Revision: snap.R(2)},
+ },
+ Current: snap.R(2),
+ Active: true,
+ })
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias5": "auto",
+ "alias3": "disabled",
+ },
+ "alias-snap2": {
+ "alias2": "enabled",
+ },
+ })
+
+ aliases, err = snapstate.Aliases(s.state)
+ c.Assert(err, IsNil)
+ c.Check(aliases, DeepEquals, map[string]map[string]string{
+ "alias-snap": {
+ "alias1": "enabled",
+ "alias5": "auto",
+ "alias3": "disabled",
+ },
+ "alias-snap2": {
+ "alias2": "enabled",
+ },
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+
+ "golang.org/x/net/context"
+)
+
+// A StoreService can find, list available updates and download snaps.
+type StoreService interface {
+ SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error)
+ Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error)
+ ListRefresh([]*store.RefreshCandidate, *auth.UserState) ([]*snap.Info, error)
+ Sections(user *auth.UserState) ([]string, error)
+ Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error
+
+ Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error)
+
+ SuggestedCurrency() string
+ Buy(options *store.BuyOptions, user *auth.UserState) (*store.BuyResult, error)
+ ReadyToBuy(*auth.UserState) error
+}
+
+type managerBackend interface {
+ // install releated
+ SetupSnap(snapFilePath string, si *snap.SideInfo, meter progress.Meter) error
+ CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error
+ LinkSnap(info *snap.Info) error
+ StartSnapServices(info *snap.Info, meter progress.Meter) error
+ StopSnapServices(info *snap.Info, meter progress.Meter) error
+
+ // the undoers for install
+ UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error
+ UndoCopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error
+ // cleanup
+ ClearTrashedData(oldSnap *snap.Info)
+
+ // remove related
+ UnlinkSnap(info *snap.Info, meter progress.Meter) error
+ RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error
+ RemoveSnapData(info *snap.Info) error
+ RemoveSnapCommonData(info *snap.Info) error
+ DiscardSnapNamespace(snapName string) error
+
+ // alias related
+ MatchingAliases(aliases []*backend.Alias) ([]*backend.Alias, error)
+ MissingAliases(aliases []*backend.Alias) ([]*backend.Alias, error)
+ UpdateAliases(add []*backend.Alias, remove []*backend.Alias) error
+ RemoveSnapAliases(snapName string) error
+
+ // testing helpers
+ CurrentInfo(cur *snap.Info)
+ Candidate(sideInfo *snap.SideInfo)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// Alias represents a command alias with a name and its application target.
+type Alias struct {
+ Name string `json:"name"`
+ Target string `json:"target"`
+}
+
+// MissingAliases returns the subset of aliases that are missing on disk.
+func (b Backend) MissingAliases(aliases []*Alias) ([]*Alias, error) {
+ var res []*Alias
+ for _, cand := range aliases {
+ _, err := os.Lstat(filepath.Join(dirs.SnapBinariesDir, cand.Name))
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return nil, err
+ }
+ res = append(res, cand)
+ }
+ }
+ return res, nil
+}
+
+// MatchingAliases returns the subset of aliases that exist on disk and have the expected targets.
+func (b Backend) MatchingAliases(aliases []*Alias) ([]*Alias, error) {
+ var res []*Alias
+ for _, cand := range aliases {
+ fn := filepath.Join(dirs.SnapBinariesDir, cand.Name)
+ fileInfo, err := os.Lstat(fn)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return nil, err
+ }
+ continue
+ }
+ if (fileInfo.Mode() & os.ModeSymlink) != 0 {
+ target, err := os.Readlink(fn)
+ if err != nil {
+ return nil, err
+ }
+ if target == cand.Target {
+ res = append(res, cand)
+ }
+ }
+ }
+ return res, nil
+}
+
+// UpdateAliases adds and removes the given aliases.
+func (b Backend) UpdateAliases(add []*Alias, remove []*Alias) error {
+ for _, alias := range add {
+ err := os.Symlink(alias.Target, filepath.Join(dirs.SnapBinariesDir, alias.Name))
+ if err != nil {
+ return fmt.Errorf("cannot create alias symlink: %v", err)
+ }
+ }
+
+ for _, alias := range remove {
+ err := os.Remove(filepath.Join(dirs.SnapBinariesDir, alias.Name))
+ if err != nil {
+ return fmt.Errorf("cannot remove alias symlink: %v", err)
+ }
+ }
+ return nil
+}
+
+// RemoveSnapAliases removes all the aliases targeting the given snap.
+func (b Backend) RemoveSnapAliases(snapName string) error {
+ cands, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
+ if err != nil {
+ return err
+ }
+ prefix := fmt.Sprintf("%s.", snapName)
+ var firstErr error
+ // best effort
+ for _, cand := range cands {
+ if osutil.IsSymlink(cand) {
+ target, err := os.Readlink(cand)
+ if err != nil {
+ if firstErr == nil {
+ firstErr = err
+ }
+ continue
+ }
+ if target == snapName || strings.HasPrefix(target, prefix) {
+ err := os.Remove(cand)
+ if err != nil && firstErr == nil {
+ firstErr = fmt.Errorf("cannot remove alias symlink: %v", err)
+ }
+ }
+ }
+ }
+ return firstErr
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+)
+
+type aliasesSuite struct {
+ be backend.Backend
+}
+
+var _ = Suite(&aliasesSuite{})
+
+func (s *aliasesSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll(dirs.SnapBinariesDir, 0755)
+ c.Assert(err, IsNil)
+}
+
+func (s *aliasesSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *aliasesSuite) TestMissingAliases(c *C) {
+ err := os.Symlink("x.foo", filepath.Join(dirs.SnapBinariesDir, "foo"))
+ c.Assert(err, IsNil)
+
+ aliases, err := s.be.MissingAliases([]*backend.Alias{{"a", "a"}, {"foo", "foo"}})
+ c.Assert(err, IsNil)
+ c.Check(aliases, DeepEquals, []*backend.Alias{{"a", "a"}})
+}
+
+func (s *aliasesSuite) TestMatchingAliases(c *C) {
+ err := os.Symlink("x.foo", filepath.Join(dirs.SnapBinariesDir, "foo"))
+ c.Assert(err, IsNil)
+ err = os.Symlink("y.bar", filepath.Join(dirs.SnapBinariesDir, "bar"))
+ c.Assert(err, IsNil)
+
+ aliases, err := s.be.MatchingAliases([]*backend.Alias{{"a", "a"}, {"foo", "x.foo"}, {"bar", "x.bar"}})
+ c.Assert(err, IsNil)
+ c.Check(aliases, DeepEquals, []*backend.Alias{{"foo", "x.foo"}})
+}
+
+func (s *aliasesSuite) TestUpdateAliasesAdd(c *C) {
+ aliases := []*backend.Alias{{"foo", "x.foo"}, {"bar", "x.bar"}}
+
+ err := s.be.UpdateAliases(aliases, nil)
+ c.Assert(err, IsNil)
+
+ match, err := s.be.MatchingAliases(aliases)
+ c.Assert(err, IsNil)
+ c.Check(match, HasLen, len(aliases))
+}
+
+func (s *aliasesSuite) TestUpdateAliasesRemove(c *C) {
+ aliases := []*backend.Alias{{"foo", "x.foo"}, {"bar", "x.bar"}}
+
+ err := s.be.UpdateAliases(aliases, nil)
+ c.Assert(err, IsNil)
+
+ match, err := s.be.MatchingAliases(aliases)
+ c.Assert(err, IsNil)
+ c.Check(match, HasLen, len(aliases))
+
+ err = s.be.UpdateAliases(nil, aliases)
+ c.Assert(err, IsNil)
+
+ missing, err := s.be.MissingAliases(aliases)
+ c.Assert(err, IsNil)
+ c.Check(missing, HasLen, len(aliases))
+
+ match, err = s.be.MatchingAliases(aliases)
+ c.Assert(err, IsNil)
+ c.Check(match, HasLen, 0)
+}
+
+func (s *aliasesSuite) TestRemoveSnapAliases(c *C) {
+ aliases := []*backend.Alias{{"x", "x"}, {"bar", "x.bar"}, {"baz", "y.baz"}, {"y", "y"}}
+
+ err := s.be.UpdateAliases(aliases, nil)
+ c.Assert(err, IsNil)
+
+ err = s.be.RemoveSnapAliases("x")
+ c.Assert(err, IsNil)
+
+ match, err := s.be.MatchingAliases(aliases)
+ c.Assert(err, IsNil)
+ c.Check(match, DeepEquals, []*backend.Alias{{"baz", "y.baz"}, {"y", "y"}})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package backend implements the low-level primitives to manage the snaps and their installation on disk.
+package backend
+
+import (
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend exposes all the low-level primitives to manage snaps and their installation on disk.
+type Backend struct{}
+
+// Candidate is a test hook.
+func (b Backend) Candidate(*snap.SideInfo) {}
+
+// CurrentInfo is a test hook.
+func (b Backend) CurrentInfo(*snap.Info) {}
+
+// OpenSnapFile opens a snap blob returning both a snap.Info completed
+// with sideInfo (if not nil) and a corresponding snap.Container.
+// Assumes the file was verified beforehand or the user asked for --dangerous.
+func OpenSnapFile(snapPath string, sideInfo *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ snapf, err := snap.Open(snapPath)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, sideInfo)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return info, snapf, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/snap/squashfs"
+
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+)
+
+func TestBackend(t *testing.T) { TestingT(t) }
+
+func makeTestSnap(c *C, snapYamlContent string) string {
+ return snaptest.MakeTestSnapWithFiles(c, snapYamlContent, nil)
+}
+
+type backendSuite struct{}
+
+var _ = Suite(&backendSuite{})
+
+func (s *backendSuite) TestOpenSnapFile(c *C) {
+ const yaml = `name: hello
+version: 1.0
+apps:
+ bin:
+ command: bin
+`
+
+ snapPath := makeTestSnap(c, yaml)
+ info, snapf, err := backend.OpenSnapFile(snapPath, nil)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapf, FitsTypeOf, &squashfs.Snap{})
+ c.Check(info.Name(), Equals, "hello")
+}
+
+func (s *backendSuite) TestOpenSnapFilebSideInfo(c *C) {
+ const yaml = `name: foo
+apps:
+ bar:
+ command: bin/bar
+plugs:
+ plug:
+slots:
+ slot:
+`
+
+ snapPath := makeTestSnap(c, yaml)
+ si := snap.SideInfo{RealName: "blessed", Revision: snap.R(42)}
+ info, _, err := backend.OpenSnapFile(snapPath, &si)
+ c.Assert(err, IsNil)
+
+ // check side info
+ c.Check(info.Name(), Equals, "blessed")
+ c.Check(info.Revision, Equals, snap.R(42))
+
+ c.Check(info.SideInfo, DeepEquals, si)
+
+ // ensure that all leaf objects link back to the same snap.Info
+ // and not to some copy.
+ // (we had a bug around this)
+ c.Check(info.Apps["bar"].Snap, Equals, info)
+ c.Check(info.Plugs["plug"].Snap, Equals, info)
+ c.Check(info.Slots["slot"].Snap, Equals, info)
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "os"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+)
+
+// CopySnapData makes a copy of oldSnap data for newSnap in its data directories.
+func (b Backend) CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error {
+ // deal with the old data or
+ // otherwise just create a empty data dir
+
+ // Make sure the common data directory exists, even if this isn't a new
+ // install.
+ if err := os.MkdirAll(newSnap.CommonDataDir(), 0755); err != nil {
+ return err
+ }
+
+ if oldSnap == nil {
+ return os.MkdirAll(newSnap.DataDir(), 0755)
+ }
+
+ return copySnapData(oldSnap, newSnap)
+}
+
+// UndoCopySnapData removes the copy that may have been done for newInfo snap of oldInfo snap data and also the data directories that may have been created for newInfo snap.
+func (b Backend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, meter progress.Meter) error {
+ err1 := b.RemoveSnapData(newInfo)
+ if err1 != nil {
+ logger.Noticef("Cannot remove data directories for %q: %v", newInfo.Name(), err1)
+ }
+
+ var err2 error
+ if oldInfo == nil {
+ // first install, remove created common data dir
+ err2 = b.RemoveSnapCommonData(newInfo)
+ if err2 != nil {
+ logger.Noticef("Cannot remove common data directories for %q: %v", newInfo.Name(), err2)
+ }
+ } else {
+ err2 = b.untrashData(newInfo)
+ if err2 != nil {
+ logger.Noticef("Cannot restore original data for %q while undoing: %v", newInfo.Name(), err2)
+ }
+ }
+
+ return firstErr(err1, err2)
+}
+
+// ClearTrashedData removes the trash. It returns no errors on the assumption that it is called very late in the game.
+func (b Backend) ClearTrashedData(oldSnap *snap.Info) {
+ dirs, err := snapDataDirs(oldSnap)
+ if err != nil {
+ logger.Noticef("Cannot remove previous data for %q: %v", oldSnap.Name(), err)
+ return
+ }
+
+ for _, d := range dirs {
+ if err := clearTrash(d); err != nil {
+ logger.Noticef("Cannot remove %s: %v", d, err)
+ }
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+)
+
+type copydataSuite struct {
+ be backend.Backend
+ nullProgress progress.NullProgress
+ tempdir string
+}
+
+var _ = Suite(©dataSuite{})
+
+func (s *copydataSuite) SetUpTest(c *C) {
+ s.tempdir = c.MkDir()
+ dirs.SetRootDir(s.tempdir)
+}
+
+func (s *copydataSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+const (
+ helloYaml1 = `name: hello
+version: 1.0
+`
+ helloYaml2 = `name: hello
+version: 2.0
+`
+ helloContents = ""
+)
+
+func (s *copydataSuite) TestCopyData(c *C) {
+ homedir := filepath.Join(s.tempdir, "home", "user1", "snap")
+ homeData := filepath.Join(homedir, "hello/10")
+ err := os.MkdirAll(homeData, 0755)
+ c.Assert(err, IsNil)
+ homeCommonData := filepath.Join(homedir, "hello/common")
+ err = os.MkdirAll(homeCommonData, 0755)
+ c.Assert(err, IsNil)
+
+ canaryData := []byte("ni ni ni")
+
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ // just creates data dirs in this case
+ err = s.be.CopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ canaryDataFile := filepath.Join(v1.DataDir(), "canary.txt")
+ err = ioutil.WriteFile(canaryDataFile, canaryData, 0644)
+ c.Assert(err, IsNil)
+ canaryDataFile = filepath.Join(v1.CommonDataDir(), "canary.common")
+ err = ioutil.WriteFile(canaryDataFile, canaryData, 0644)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(homeData, "canary.home"), canaryData, 0644)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(homeCommonData, "canary.common_home"), canaryData, 0644)
+ c.Assert(err, IsNil)
+
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+ err = s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ newCanaryDataFile := filepath.Join(dirs.SnapDataDir, "hello/20", "canary.txt")
+ content, err := ioutil.ReadFile(newCanaryDataFile)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, canaryData)
+
+ // ensure common data file is still there (even though it didn't get copied)
+ newCanaryDataFile = filepath.Join(dirs.SnapDataDir, "hello", "common", "canary.common")
+ content, err = ioutil.ReadFile(newCanaryDataFile)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, canaryData)
+
+ newCanaryDataFile = filepath.Join(homedir, "hello/20", "canary.home")
+ content, err = ioutil.ReadFile(newCanaryDataFile)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, canaryData)
+
+ // ensure home common data file is still there (even though it didn't get copied)
+ newCanaryDataFile = filepath.Join(homedir, "hello", "common", "canary.common_home")
+ content, err = ioutil.ReadFile(newCanaryDataFile)
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, canaryData)
+}
+
+func (s *copydataSuite) TestCopyDataBails(c *C) {
+ oldSnapDataHomeGlob := dirs.SnapDataHomeGlob
+ defer func() { dirs.SnapDataHomeGlob = oldSnapDataHomeGlob }()
+
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ c.Assert(s.be.CopySnapData(v1, nil, &s.nullProgress), IsNil)
+ c.Assert(os.Chmod(v1.DataDir(), 0), IsNil)
+
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+ err := s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Check(err, ErrorMatches, "cannot copy .*")
+}
+
+// ensure that even with no home dir there is no error and the
+// system data gets copied
+func (s *copydataSuite) TestCopyDataNoUserHomes(c *C) {
+ // this home dir path does not exist
+ oldSnapDataHomeGlob := dirs.SnapDataHomeGlob
+ defer func() { dirs.SnapDataHomeGlob = oldSnapDataHomeGlob }()
+ dirs.SnapDataHomeGlob = filepath.Join(s.tempdir, "no-such-home", "*", "snap")
+
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ err := s.be.CopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ canaryDataFile := filepath.Join(v1.DataDir(), "canary.txt")
+ err = ioutil.WriteFile(canaryDataFile, []byte(""), 0644)
+ c.Assert(err, IsNil)
+ canaryDataFile = filepath.Join(v1.CommonDataDir(), "canary.common")
+ err = ioutil.WriteFile(canaryDataFile, []byte(""), 0644)
+ c.Assert(err, IsNil)
+
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+ err = s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ _, err = os.Stat(filepath.Join(v2.DataDir(), "canary.txt"))
+ c.Assert(err, IsNil)
+ _, err = os.Stat(filepath.Join(v2.CommonDataDir(), "canary.common"))
+ c.Assert(err, IsNil)
+
+ // sanity atm
+ c.Check(v1.DataDir(), Not(Equals), v2.DataDir())
+ c.Check(v1.CommonDataDir(), Equals, v2.CommonDataDir())
+}
+
+func (s *copydataSuite) populateData(c *C, revision snap.Revision) {
+ datadir := filepath.Join(dirs.SnapDataDir, "hello", revision.String())
+ subdir := filepath.Join(datadir, "random-subdir")
+ err := os.MkdirAll(subdir, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(subdir, "canary"), []byte(fmt.Sprintln(revision)), 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *copydataSuite) populatedData(d string) string {
+ bs, err := ioutil.ReadFile(filepath.Join(dirs.SnapDataDir, "hello", d, "random-subdir", "canary"))
+ if err == nil {
+ return string(bs)
+ }
+ if os.IsNotExist(err) {
+ return ""
+ }
+ panic(err)
+}
+
+func (s copydataSuite) populateHomeData(c *C, user string, revision snap.Revision) (homedir string) {
+ homedir = filepath.Join(s.tempdir, "home", user, "snap")
+ homeData := filepath.Join(homedir, "hello", revision.String())
+ err := os.MkdirAll(homeData, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(homeData, "canary.home"), []byte(fmt.Sprintln(revision)), 0644)
+ c.Assert(err, IsNil)
+ return
+}
+
+func (s *copydataSuite) TestCopyDataDoUndo(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+ homedir := s.populateHomeData(c, "user1", snap.R(10))
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+
+ // copy data
+ err := s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+ v2data := filepath.Join(dirs.SnapDataDir, "hello/20")
+ l, err := filepath.Glob(filepath.Join(v2data, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+ v2HomeData := filepath.Join(homedir, "hello/20")
+ l, err = filepath.Glob(filepath.Join(v2HomeData, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+
+ err = s.be.UndoCopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // now removed
+ _, err = os.Stat(v2data)
+ c.Assert(os.IsNotExist(err), Equals, true)
+ _, err = os.Stat(v2HomeData)
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *copydataSuite) TestCopyDataDoUndoNoUserHomes(c *C) {
+ // this home dir path does not exist
+ oldSnapDataHomeGlob := dirs.SnapDataHomeGlob
+ defer func() { dirs.SnapDataHomeGlob = oldSnapDataHomeGlob }()
+ dirs.SnapDataHomeGlob = filepath.Join(s.tempdir, "no-such-home", "*", "snap")
+
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+
+ // copy data
+ err := s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+ v2data := filepath.Join(dirs.SnapDataDir, "hello/20")
+ l, err := filepath.Glob(filepath.Join(v2data, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+
+ err = s.be.UndoCopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // now removed
+ _, err = os.Stat(v2data)
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *copydataSuite) TestCopyDataDoUndoFirstInstall(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+
+ // first install
+ err := s.be.CopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.DataDir())
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.CommonDataDir())
+ c.Assert(err, IsNil)
+
+ err = s.be.UndoCopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.DataDir())
+ c.Check(os.IsNotExist(err), Equals, true)
+ _, err = os.Stat(v1.CommonDataDir())
+ c.Check(os.IsNotExist(err), Equals, true)
+}
+
+func (s *copydataSuite) TestCopyDataDoABA(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+ c.Check(s.populatedData("10"), Equals, "10\n")
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+ // and write our own data to it
+ s.populateData(c, snap.R(20))
+ c.Check(s.populatedData("20"), Equals, "20\n")
+
+ // and now we pretend to refresh back to v1 (r10)
+ c.Check(s.be.CopySnapData(v1, v2, &s.nullProgress), IsNil)
+
+ // so 10 now has 20's data
+ c.Check(s.populatedData("10"), Equals, "20\n")
+
+ // but we still have the trash
+ c.Check(s.populatedData("10.old"), Equals, "10\n")
+
+ // but cleanup cleans it up, huzzah
+ s.be.ClearTrashedData(v1)
+ c.Check(s.populatedData("10.old"), Equals, "")
+}
+
+func (s *copydataSuite) TestCopyDataDoUndoABA(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+ c.Check(s.populatedData("10"), Equals, "10\n")
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+ // and write our own data to it
+ s.populateData(c, snap.R(20))
+ c.Check(s.populatedData("20"), Equals, "20\n")
+
+ // and now we pretend to refresh back to v1 (r10)
+ c.Check(s.be.CopySnapData(v1, v2, &s.nullProgress), IsNil)
+
+ // so v1 (r10) now has v2 (r20)'s data and we have trash
+ c.Check(s.populatedData("10"), Equals, "20\n")
+ c.Check(s.populatedData("10.old"), Equals, "10\n")
+
+ // but oh no! we have to undo it!
+ c.Check(s.be.UndoCopySnapData(v1, v2, &s.nullProgress), IsNil)
+
+ // so now v1 (r10) has v1 (r10)'s data and v2 (r20) has v2 (r20)'s data and we have no trash
+ c.Check(s.populatedData("10"), Equals, "10\n")
+ c.Check(s.populatedData("20"), Equals, "20\n")
+ c.Check(s.populatedData("10.old"), Equals, "")
+}
+
+func (s *copydataSuite) TestCopyDataDoIdempotent(c *C) {
+ // make sure that a retry wouldn't stumble on partial work
+
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+
+ s.populateData(c, snap.R(10))
+ homedir := s.populateHomeData(c, "user1", snap.R(10))
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+
+ // copy data
+ err := s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ err = s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ v2data := filepath.Join(dirs.SnapDataDir, "hello/20")
+ l, err := filepath.Glob(filepath.Join(v2data, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+ v2HomeData := filepath.Join(homedir, "hello/20")
+ l, err = filepath.Glob(filepath.Join(v2HomeData, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+}
+
+func (s *copydataSuite) TestCopyDataUndoIdempotent(c *C) {
+ // make sure that a retry wouldn't stumble on partial work
+
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+ homedir := s.populateHomeData(c, "user1", snap.R(10))
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+
+ // copy data
+ err := s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ v2data := filepath.Join(dirs.SnapDataDir, "hello/20")
+
+ err = s.be.UndoCopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ err = s.be.UndoCopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // now removed
+ _, err = os.Stat(v2data)
+ c.Assert(os.IsNotExist(err), Equals, true)
+ v2HomeData := filepath.Join(homedir, "hello/20")
+ _, err = os.Stat(v2HomeData)
+ c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *copydataSuite) TestCopyDataDoFirstInstallIdempotent(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+
+ // first install
+ err := s.be.CopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ err = s.be.CopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ _, err = os.Stat(v1.DataDir())
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.CommonDataDir())
+ c.Assert(err, IsNil)
+
+ err = s.be.UndoCopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.DataDir())
+ c.Check(os.IsNotExist(err), Equals, true)
+ _, err = os.Stat(v1.CommonDataDir())
+ c.Check(os.IsNotExist(err), Equals, true)
+}
+
+func (s *copydataSuite) TestCopyDataUndoFirstInstallIdempotent(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+
+ // first install
+ err := s.be.CopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.DataDir())
+ c.Assert(err, IsNil)
+ _, err = os.Stat(v1.CommonDataDir())
+ c.Assert(err, IsNil)
+
+ err = s.be.UndoCopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ err = s.be.UndoCopySnapData(v1, nil, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ _, err = os.Stat(v1.DataDir())
+ c.Check(os.IsNotExist(err), Equals, true)
+ _, err = os.Stat(v1.CommonDataDir())
+ c.Check(os.IsNotExist(err), Equals, true)
+}
+
+func (s *copydataSuite) TestCopyDataCopyFailure(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, helloContents, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, helloContents, &snap.SideInfo{Revision: snap.R(20)})
+
+ defer testutil.MockCommand(c, "cp", "echo cp: boom; exit 3").Restore()
+
+ q := func(s string) string {
+ return regexp.QuoteMeta(strconv.Quote(s))
+ }
+
+ // copy data will fail
+ err := s.be.CopySnapData(v2, v1, &s.nullProgress)
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot copy %s to %s: .*: "cp: boom" \(3\)`, q(v1.DataDir()), q(v2.DataDir())))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+var (
+ AddMountUnit = addMountUnit
+ RemoveMountUnit = removeMountUnit
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/wrappers"
+)
+
+func updateCurrentSymlinks(info *snap.Info) error {
+ mountDir := info.MountDir()
+
+ currentActiveSymlink := filepath.Join(mountDir, "..", "current")
+ if err := os.Remove(currentActiveSymlink); err != nil && !os.IsNotExist(err) {
+ logger.Noticef("Cannot remove %q: %v", currentActiveSymlink, err)
+ }
+
+ dataDir := info.DataDir()
+ currentDataSymlink := filepath.Join(dataDir, "..", "current")
+ if err := os.Remove(currentDataSymlink); err != nil && !os.IsNotExist(err) {
+ logger.Noticef("Cannot remove %q: %v", currentDataSymlink, err)
+ }
+
+ if err := os.MkdirAll(info.DataDir(), 0755); err != nil {
+ return err
+ }
+
+ if err := os.Symlink(filepath.Base(dataDir), currentDataSymlink); err != nil {
+ return err
+ }
+
+ return os.Symlink(filepath.Base(mountDir), currentActiveSymlink)
+}
+
+// LinkSnap makes the snap available by generating wrappers and setting the current symlinks.
+func (b Backend) LinkSnap(info *snap.Info) error {
+ if err := generateWrappers(info); err != nil {
+ return err
+ }
+
+ // XXX/TODO: this needs to be a task with proper undo and tests!
+ if err := boot.SetNextBoot(info); err != nil {
+ return err
+ }
+
+ return updateCurrentSymlinks(info)
+}
+
+func (b Backend) StartSnapServices(info *snap.Info, meter progress.Meter) error {
+ return wrappers.StartSnapServices(info, meter)
+}
+
+func (b Backend) StopSnapServices(info *snap.Info, meter progress.Meter) error {
+ return wrappers.StopSnapServices(info, meter)
+}
+
+func generateWrappers(s *snap.Info) error {
+ // add the CLI apps from the snap.yaml
+ if err := wrappers.AddSnapBinaries(s); err != nil {
+ return err
+ }
+ // add the daemons from the snap.yaml
+ if err := wrappers.AddSnapServices(s, &progress.NullProgress{}); err != nil {
+ return err
+ }
+ // add the desktop files
+ if err := wrappers.AddSnapDesktopFiles(s); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func removeGeneratedWrappers(s *snap.Info, meter progress.Meter) error {
+ err1 := wrappers.RemoveSnapBinaries(s)
+ if err1 != nil {
+ logger.Noticef("Cannot remove binaries for %q: %v", s.Name(), err1)
+ }
+
+ err2 := wrappers.RemoveSnapServices(s, meter)
+ if err2 != nil {
+ logger.Noticef("Cannot remove services for %q: %v", s.Name(), err2)
+ }
+
+ err3 := wrappers.RemoveSnapDesktopFiles(s)
+ if err3 != nil {
+ logger.Noticef("Cannot remove desktop files for %q: %v", s.Name(), err3)
+ }
+
+ return firstErr(err1, err2, err3)
+}
+
+// UnlinkSnap makes the snap unavailable to the system removing wrappers and symlinks.
+func (b Backend) UnlinkSnap(info *snap.Info, meter progress.Meter) error {
+ // remove generated services, binaries etc
+ err1 := removeGeneratedWrappers(info, meter)
+
+ // and finally remove current symlinks
+ err2 := removeCurrentSymlinks(info)
+
+ // FIXME: aggregate errors instead
+ return firstErr(err1, err2)
+}
+
+func removeCurrentSymlinks(info snap.PlaceInfo) error {
+ var err1, err2 error
+
+ // the snap "current" symlink
+ currentActiveSymlink := filepath.Join(info.MountDir(), "..", "current")
+ err1 = os.Remove(currentActiveSymlink)
+ if err1 != nil && !os.IsNotExist(err1) {
+ logger.Noticef("Cannot remove %q: %v", currentActiveSymlink, err1)
+ } else {
+ err1 = nil
+ }
+
+ // the data "current" symlink
+ currentDataSymlink := filepath.Join(info.DataDir(), "..", "current")
+ err2 = os.Remove(currentDataSymlink)
+ if err2 != nil && !os.IsNotExist(err2) {
+ logger.Noticef("Cannot remove %q: %v", currentDataSymlink, err2)
+ } else {
+ err2 = nil
+ }
+
+ if err1 != nil && err2 != nil {
+ return fmt.Errorf("cannot remove snap current symlink: %v and %v", err1, err2)
+ } else if err1 != nil {
+ return fmt.Errorf("cannot remove snap current symlink: %v", err1)
+ } else if err2 != nil {
+ return fmt.Errorf("cannot remove snap current symlink: %v", err2)
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/systemd"
+
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+)
+
+type linkSuite struct {
+ be backend.Backend
+ nullProgress progress.NullProgress
+ prevctlCmd func(...string) ([]byte, error)
+}
+
+var _ = Suite(&linkSuite{})
+
+func (s *linkSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ s.prevctlCmd = systemd.SystemctlCmd
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ return []byte("ActiveState=inactive\n"), nil
+ }
+}
+
+func (s *linkSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ systemd.SystemctlCmd = s.prevctlCmd
+}
+
+func (s *linkSuite) TestLinkDoUndoGenerateWrappers(c *C) {
+ const yaml = `name: hello
+version: 1.0
+environment:
+ KEY: value
+
+apps:
+ bin:
+ command: bin
+ svc:
+ command: svc
+ daemon: simple
+`
+ const contents = ""
+
+ info := snaptest.MockSnap(c, yaml, contents, &snap.SideInfo{Revision: snap.R(11)})
+
+ err := s.be.LinkSnap(info)
+ c.Assert(err, IsNil)
+
+ l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+ l, err = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.service"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+
+ // undo will remove
+ err = s.be.UnlinkSnap(info, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ l, err = filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 0)
+ l, err = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.service"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 0)
+}
+
+func (s *linkSuite) TestLinkDoUndoCurrentSymlink(c *C) {
+ const yaml = `name: hello
+version: 1.0
+`
+ const contents = ""
+
+ info := snaptest.MockSnap(c, yaml, contents, &snap.SideInfo{Revision: snap.R(11)})
+
+ err := s.be.LinkSnap(info)
+ c.Assert(err, IsNil)
+
+ mountDir := info.MountDir()
+ dataDir := info.DataDir()
+ currentActiveSymlink := filepath.Join(mountDir, "..", "current")
+ currentActiveDir, err := filepath.EvalSymlinks(currentActiveSymlink)
+ c.Assert(err, IsNil)
+ c.Assert(currentActiveDir, Equals, mountDir)
+
+ currentDataSymlink := filepath.Join(dataDir, "..", "current")
+ currentDataDir, err := filepath.EvalSymlinks(currentDataSymlink)
+ c.Assert(err, IsNil)
+ c.Assert(currentDataDir, Equals, dataDir)
+
+ // undo will remove the symlinks
+ err = s.be.UnlinkSnap(info, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ c.Check(osutil.FileExists(currentActiveSymlink), Equals, false)
+ c.Check(osutil.FileExists(currentDataSymlink), Equals, false)
+
+}
+
+func (s *linkSuite) TestLinkDoIdempotent(c *C) {
+ // make sure that a retry wouldn't stumble on partial work
+
+ const yaml = `name: hello
+version: 1.0
+environment:
+ KEY: value
+apps:
+ bin:
+ command: bin
+ svc:
+ command: svc
+ daemon: simple
+`
+ const contents = ""
+
+ info := snaptest.MockSnap(c, yaml, contents, &snap.SideInfo{Revision: snap.R(11)})
+
+ err := s.be.LinkSnap(info)
+ c.Assert(err, IsNil)
+
+ err = s.be.LinkSnap(info)
+ c.Assert(err, IsNil)
+
+ l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+ l, err = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.service"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 1)
+
+ mountDir := info.MountDir()
+ dataDir := info.DataDir()
+ currentActiveSymlink := filepath.Join(mountDir, "..", "current")
+ currentActiveDir, err := filepath.EvalSymlinks(currentActiveSymlink)
+ c.Assert(err, IsNil)
+ c.Assert(currentActiveDir, Equals, mountDir)
+
+ currentDataSymlink := filepath.Join(dataDir, "..", "current")
+ currentDataDir, err := filepath.EvalSymlinks(currentDataSymlink)
+ c.Assert(err, IsNil)
+ c.Assert(currentDataDir, Equals, dataDir)
+}
+
+func (s *linkSuite) TestLinkUndoIdempotent(c *C) {
+ // make sure that a retry wouldn't stumble on partial work
+
+ const yaml = `name: hello
+version: 1.0
+apps:
+ bin:
+ command: bin
+ svc:
+ command: svc
+ daemon: simple
+`
+ const contents = ""
+
+ info := snaptest.MockSnap(c, yaml, contents, &snap.SideInfo{Revision: snap.R(11)})
+
+ err := s.be.LinkSnap(info)
+ c.Assert(err, IsNil)
+
+ err = s.be.UnlinkSnap(info, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ err = s.be.UnlinkSnap(info, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // no wrappers
+ l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 0)
+ l, err = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.service"))
+ c.Assert(err, IsNil)
+ c.Assert(l, HasLen, 0)
+
+ // no symlinks
+ currentActiveSymlink := filepath.Join(info.MountDir(), "..", "current")
+ currentDataSymlink := filepath.Join(info.DataDir(), "..", "current")
+ c.Check(osutil.FileExists(currentActiveSymlink), Equals, false)
+ c.Check(osutil.FileExists(currentDataSymlink), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/systemd"
+)
+
+func addMountUnit(s *snap.Info, meter progress.Meter) error {
+ squashfsPath := dirs.StripRootDir(s.MountFile())
+ whereDir := dirs.StripRootDir(s.MountDir())
+
+ sysd := systemd.New(dirs.GlobalRootDir, meter)
+ mountUnitName, err := sysd.WriteMountUnitFile(s.Name(), squashfsPath, whereDir, "squashfs")
+ if err != nil {
+ return err
+ }
+
+ // we need to do a daemon-reload here to ensure that systemd really
+ // knows about this new mount unit file
+ if err := sysd.DaemonReload(); err != nil {
+ return err
+ }
+
+ if err := sysd.Enable(mountUnitName); err != nil {
+ return err
+ }
+
+ return sysd.Start(mountUnitName)
+}
+
+func removeMountUnit(baseDir string, meter progress.Meter) error {
+ sysd := systemd.New(dirs.GlobalRootDir, meter)
+ unit := systemd.MountUnitPath(dirs.StripRootDir(baseDir))
+ if osutil.FileExists(unit) {
+ // use umount -d (cleanup loopback devices) -l (lazy) to ensure that even busy mount points
+ // can be unmounted.
+ // note that the long option --lazy is not supported on trusty.
+ // the explicit -d is only needed on trusty.
+ isMounted, err := osutil.IsMounted(baseDir)
+ if err != nil {
+ return err
+ }
+ if isMounted {
+ if output, err := exec.Command("umount", "-d", "-l", baseDir).CombinedOutput(); err != nil {
+ return osutil.OutputErr(output, err)
+ }
+
+ if err := sysd.Stop(filepath.Base(unit), time.Duration(1*time.Second)); err != nil {
+ return err
+ }
+ }
+ if err := sysd.Disable(filepath.Base(unit)); err != nil {
+ return err
+ }
+ if err := os.Remove(unit); err != nil {
+ return err
+ }
+ // daemon-reload to ensure that systemd actually really
+ // forgets about this mount unit
+ if err := sysd.DaemonReload(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type mountunitSuite struct {
+ nullProgress progress.NullProgress
+ prevctlCmd func(...string) ([]byte, error)
+ umount *testutil.MockCmd
+}
+
+var _ = Suite(&mountunitSuite{})
+
+func (s *mountunitSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc", "systemd", "system", "multi-user.target.wants"), 0755)
+ c.Assert(err, IsNil)
+
+ s.prevctlCmd = systemd.SystemctlCmd
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ return []byte("ActiveState=inactive\n"), nil
+ }
+ s.umount = testutil.MockCommand(c, "umount", "")
+}
+
+func (s *mountunitSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ systemd.SystemctlCmd = s.prevctlCmd
+ s.umount.Restore()
+}
+
+func (s *mountunitSuite) TestAddMountUnit(c *C) {
+ info := &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(13),
+ },
+ Version: "1.1",
+ Architectures: []string{"all"},
+ }
+ err := backend.AddMountUnit(info, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // ensure correct mount unit
+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, "snap-foo-13.mount"))
+ c.Assert(err, IsNil)
+ c.Assert(string(mount), Equals, `[Unit]
+Description=Mount unit for foo
+
+[Mount]
+What=/var/lib/snapd/snaps/foo_13.snap
+Where=/snap/foo/13
+Type=squashfs
+
+[Install]
+WantedBy=multi-user.target
+`)
+
+}
+
+func (s *mountunitSuite) TestRemoveMountUnit(c *C) {
+ info := &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(13),
+ },
+ Version: "1.1",
+ Architectures: []string{"all"},
+ }
+
+ err := backend.AddMountUnit(info, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // ensure we have the files
+ p := filepath.Join(dirs.SnapServicesDir, "snap-foo-13.mount")
+ c.Assert(osutil.FileExists(p), Equals, true)
+
+ // now call remove and ensure they are gone
+ err = backend.RemoveMountUnit(info.MountDir(), &s.nullProgress)
+ c.Assert(err, IsNil)
+ p = filepath.Join(dirs.SnapServicesDir, "snaps-foo-13.mount")
+ c.Assert(osutil.FileExists(p), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "fmt"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// mountNsPath returns path of the mount namespace file of a given snap
+func mountNsPath(snapName string) string {
+ // NOTE: This value has to be synchronized with snap-confine
+ return filepath.Join(dirs.SnapRunNsDir, fmt.Sprintf("%s.mnt", snapName))
+}
+
+func (b Backend) DiscardSnapNamespace(snapName string) error {
+ mntFile := mountNsPath(snapName)
+ // If there's a .mnt file that was created by snap-confine we should ask
+ // snap-confine to discard it appropriately.
+ if osutil.FileExists(mntFile) {
+ snapDiscardNs := filepath.Join(dirs.LibExecDir, "snap-discard-ns")
+ cmd := exec.Command(snapDiscardNs, snapName)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("cannot discard preserved namespaces of snap %q: %s", snapName, osutil.OutputErr(output, err))
+ }
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type nsSuite struct {
+ be backend.Backend
+ nullProgress progress.NullProgress
+ oldLibExecDir string
+}
+
+var _ = Suite(&nsSuite{})
+
+func (s *nsSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ // Mock enough bits so that we can observe calls to snap-discard-ns
+ s.oldLibExecDir = dirs.LibExecDir
+}
+
+func (s *nsSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ dirs.LibExecDir = s.oldLibExecDir
+}
+
+func (s *nsSuite) TestDiscardNamespaceMntFilePresent(c *C) {
+ // Mock the snap-discard-ns command
+ cmd := testutil.MockCommand(c, "snap-discard-ns", "")
+ dirs.LibExecDir = cmd.BinDir()
+ defer cmd.Restore()
+
+ // the presence of the .mnt file is the trigger so create it now
+ c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil)
+ c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil)
+
+ err := s.be.DiscardSnapNamespace("snap-name")
+ c.Assert(err, IsNil)
+ c.Check(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "snap-name"}})
+}
+
+func (s *nsSuite) TestDiscardNamespaceMntFileAbsent(c *C) {
+ // Mock the snap-discard-ns command
+ cmd := testutil.MockCommand(c, "snap-discard-ns", "")
+ dirs.LibExecDir = cmd.BinDir()
+ defer cmd.Restore()
+
+ // don't create the .mnt file that triggers the discard operation
+
+ // ask the backend to discard the namespace
+ err := s.be.DiscardSnapNamespace("snap-name")
+ c.Assert(err, IsNil)
+ c.Check(cmd.Calls(), IsNil)
+}
+
+func (s *nsSuite) TestDiscardNamespaceFailure(c *C) {
+ // Mock the snap-discard-ns command, make it fail
+ cmd := testutil.MockCommand(c, "snap-discard-ns", "echo failure; exit 1;")
+ dirs.LibExecDir = cmd.BinDir()
+ defer cmd.Restore()
+
+ // the presence of the .mnt file is the trigger so create it now
+ c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil)
+ c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil)
+
+ // ask the backend to discard the namespace
+ err := s.be.DiscardSnapNamespace("snap-name")
+ c.Assert(err, ErrorMatches, `cannot discard preserved namespaces of snap "snap-name": failure`)
+ c.Check(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "snap-name"}})
+}
+
+func (s *nsSuite) TestDiscardNamespaceSilentFailure(c *C) {
+ // Mock the snap-discard-ns command, make it fail
+ cmd := testutil.MockCommand(c, "snap-discard-ns", "exit 1")
+ dirs.LibExecDir = cmd.BinDir()
+ defer cmd.Restore()
+
+ // the presence of the .mnt file is the trigger so create it now
+ c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil)
+ c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil)
+
+ // ask the backend to discard the namespace
+ err := s.be.DiscardSnapNamespace("snap-name")
+ c.Assert(err, ErrorMatches, `cannot discard preserved namespaces of snap "snap-name": exit status 1`)
+ c.Check(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "snap-name"}})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+)
+
+// SetupSnap does prepare and mount the snap for further processing.
+func (b Backend) SetupSnap(snapFilePath string, sideInfo *snap.SideInfo, meter progress.Meter) error {
+ // This assumes that the snap was already verified or --dangerous was used.
+
+ s, snapf, err := OpenSnapFile(snapFilePath, sideInfo)
+ if err != nil {
+ return err
+ }
+ instdir := s.MountDir()
+
+ if err := os.MkdirAll(instdir, 0755); err != nil {
+ return err
+ }
+
+ if err := snapf.Install(s.MountFile(), instdir); err != nil {
+ return err
+ }
+
+ // generate the mount unit for the squashfs
+ if err := addMountUnit(s, meter); err != nil {
+ return err
+ }
+
+ if s.Type == snap.TypeKernel {
+ if err := boot.ExtractKernelAssets(s, snapf); err != nil {
+ return fmt.Errorf("cannot install kernel: %s", err)
+ }
+ }
+
+ return err
+}
+
+// RemoveSnapFiles removes the snap files from the disk after unmounting the snap.
+func (b Backend) RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error {
+ mountDir := s.MountDir()
+
+ // this also ensures that the mount unit stops
+ if err := removeMountUnit(mountDir, meter); err != nil {
+ return err
+ }
+
+ if err := os.RemoveAll(mountDir); err != nil {
+ return err
+ }
+
+ // try to remove parent dir, failure is ok, means some other
+ // revisions are still in there
+ os.Remove(filepath.Dir(mountDir))
+
+ // snapPath may either be a file or a (broken) symlink to a dir
+ snapPath := s.MountFile()
+ if _, err := os.Lstat(snapPath); err == nil {
+ // remove the kernel assets (if any)
+ if typ == snap.TypeKernel {
+ if err := boot.RemoveKernelAssets(s); err != nil {
+ return err
+ }
+ }
+
+ // remove the snap
+ if err := os.RemoveAll(snapPath); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// UndoSetupSnap undoes the work of SetupSnap using RemoveSnapFiles.
+func (b Backend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error {
+ return b.RemoveSnapFiles(s, typ, meter)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type setupSuite struct {
+ be backend.Backend
+ nullProgress progress.NullProgress
+ prevctlCmd func(...string) ([]byte, error)
+ umount *testutil.MockCmd
+}
+
+var _ = Suite(&setupSuite{})
+
+func (s *setupSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc", "systemd", "system", "multi-user.target.wants"), 0755)
+ c.Assert(err, IsNil)
+
+ s.prevctlCmd = systemd.SystemctlCmd
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ return []byte("ActiveState=inactive\n"), nil
+ }
+ s.umount = testutil.MockCommand(c, "umount", "")
+}
+
+func (s *setupSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ partition.ForceBootloader(nil)
+ systemd.SystemctlCmd = s.prevctlCmd
+ s.umount.Restore()
+}
+
+func (s *setupSuite) TestSetupDoUndoSimple(c *C) {
+ snapPath := makeTestSnap(c, helloYaml1)
+
+ si := snap.SideInfo{
+ RealName: "hello",
+ Revision: snap.R(14),
+ }
+
+ err := s.be.SetupSnap(snapPath, &si, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // after setup the snap file is in the right dir
+ c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "hello_14.snap")), Equals, true)
+
+ // ensure the right unit is created
+ mup := systemd.MountUnitPath("/snap/hello/14")
+ content, err := ioutil.ReadFile(mup)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Matches, "(?ms).*^Where=/snap/hello/14")
+ c.Assert(string(content), Matches, "(?ms).*^What=/var/lib/snapd/snaps/hello_14.snap")
+
+ minInfo := snap.MinimalPlaceInfo("hello", snap.R(14))
+ // mount dir was created
+ c.Assert(osutil.FileExists(minInfo.MountDir()), Equals, true)
+
+ // undo undoes the mount unit and the instdir creation
+ err = s.be.UndoSetupSnap(minInfo, "app", &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ l, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.mount"))
+ c.Assert(l, HasLen, 0)
+ c.Assert(osutil.FileExists(minInfo.MountDir()), Equals, false)
+
+ c.Assert(osutil.FileExists(minInfo.MountFile()), Equals, false)
+
+}
+
+func (s *setupSuite) TestSetupDoUndoKernelUboot(c *C) {
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ // we don't get real mounting
+ os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+ defer os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
+
+ testFiles := [][]string{
+ {"kernel.img", "kernel"},
+ {"initrd.img", "initrd"},
+ {"modules/4.4.0-14-generic/foo.ko", "a module"},
+ {"firmware/bar.bin", "some firmware"},
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ snapPath := snaptest.MakeTestSnapWithFiles(c, `name: kernel
+version: 1.0
+type: kernel
+`, testFiles)
+
+ si := snap.SideInfo{
+ RealName: "kernel",
+ Revision: snap.R(140),
+ }
+
+ err := s.be.SetupSnap(snapPath, &si, &s.nullProgress)
+ c.Assert(err, IsNil)
+ l, _ := filepath.Glob(filepath.Join(bootloader.Dir(), "*"))
+ c.Assert(l, HasLen, 1)
+
+ minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140))
+
+ // undo deletes the kernel assets again
+ err = s.be.UndoSetupSnap(minInfo, "kernel", &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ l, _ = filepath.Glob(filepath.Join(bootloader.Dir(), "*"))
+ c.Assert(l, HasLen, 0)
+}
+
+func (s *setupSuite) TestSetupDoIdempotent(c *C) {
+ // make sure that a retry wouldn't stumble on partial work
+ // use a kernel because that does and need to do strictly more
+
+ // this cannot check systemd own behavior though around mounts!
+
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ // we don't get real mounting
+ os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+ defer os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
+
+ testFiles := [][]string{
+ {"kernel.img", "kernel"},
+ {"initrd.img", "initrd"},
+ {"modules/4.4.0-14-generic/foo.ko", "a module"},
+ {"firmware/bar.bin", "some firmware"},
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ snapPath := snaptest.MakeTestSnapWithFiles(c, `name: kernel
+version: 1.0
+type: kernel
+`, testFiles)
+
+ si := snap.SideInfo{
+ RealName: "kernel",
+ Revision: snap.R(140),
+ }
+
+ err := s.be.SetupSnap(snapPath, &si, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // retry run
+ err = s.be.SetupSnap(snapPath, &si, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140))
+
+ // sanity checks
+ l, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.mount"))
+ c.Assert(l, HasLen, 1)
+ c.Assert(osutil.FileExists(minInfo.MountDir()), Equals, true)
+
+ c.Assert(osutil.FileExists(minInfo.MountFile()), Equals, true)
+
+ l, _ = filepath.Glob(filepath.Join(bootloader.Dir(), "*"))
+ c.Assert(l, HasLen, 1)
+}
+
+func (s *setupSuite) TestSetupUndoIdempotent(c *C) {
+ // make sure that a retry wouldn't stumble on partial work
+ // use a kernel because that does and need to do strictly more
+
+ // this cannot check systemd own behavior though around mounts!
+
+ bootloader := boottest.NewMockBootloader("mock", c.MkDir())
+ partition.ForceBootloader(bootloader)
+ // we don't get real mounting
+ os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+ defer os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
+
+ testFiles := [][]string{
+ {"kernel.img", "kernel"},
+ {"initrd.img", "initrd"},
+ {"modules/4.4.0-14-generic/foo.ko", "a module"},
+ {"firmware/bar.bin", "some firmware"},
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ snapPath := snaptest.MakeTestSnapWithFiles(c, `name: kernel
+version: 1.0
+type: kernel
+`, testFiles)
+
+ si := snap.SideInfo{
+ RealName: "kernel",
+ Revision: snap.R(140),
+ }
+
+ err := s.be.SetupSnap(snapPath, &si, &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140))
+
+ err = s.be.UndoSetupSnap(minInfo, "kernel", &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // retry run
+ err = s.be.UndoSetupSnap(minInfo, "kernel", &s.nullProgress)
+ c.Assert(err, IsNil)
+
+ // sanity checks
+ l, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.mount"))
+ c.Assert(l, HasLen, 0)
+ c.Assert(osutil.FileExists(minInfo.MountDir()), Equals, false)
+
+ c.Assert(osutil.FileExists(minInfo.MountFile()), Equals, false)
+
+ l, _ = filepath.Glob(filepath.Join(bootloader.Dir(), "*"))
+ c.Assert(l, HasLen, 0)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ unix "syscall"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// RemoveSnapData removes the data for the given version of the given snap.
+func (b Backend) RemoveSnapData(snap *snap.Info) error {
+ dirs, err := snapDataDirs(snap)
+ if err != nil {
+ return err
+ }
+
+ return removeDirs(dirs)
+}
+
+// RemoveSnapCommonData removes the data common between versions of the given snap.
+func (b Backend) RemoveSnapCommonData(snap *snap.Info) error {
+ dirs, err := snapCommonDataDirs(snap)
+ if err != nil {
+ return err
+ }
+
+ return removeDirs(dirs)
+}
+
+func (b Backend) untrashData(snap *snap.Info) error {
+ dirs, err := snapDataDirs(snap)
+ if err != nil {
+ return err
+ }
+
+ for _, d := range dirs {
+ if e := untrash(d); e != nil {
+ err = e
+ }
+ }
+
+ return err
+}
+
+func removeDirs(dirs []string) error {
+ for _, dir := range dirs {
+ if err := os.RemoveAll(dir); err != nil {
+ return err
+ }
+
+ // Attempt to remove the parent directory as well (ignore any failure)
+ os.Remove(filepath.Dir(dir))
+ }
+
+ return nil
+}
+
+// snapDataDirs returns the list of data directories for the given snap version
+func snapDataDirs(snap *snap.Info) ([]string, error) {
+ // collect the directories, homes first
+ found, err := filepath.Glob(snap.DataHomeDir())
+ if err != nil {
+ return nil, err
+ }
+ // then system data
+ found = append(found, snap.DataDir())
+
+ return found, nil
+}
+
+// snapCommonDataDirs returns the list of data directories common between versions of the given snap
+func snapCommonDataDirs(snap *snap.Info) ([]string, error) {
+ // collect the directories, homes first
+ found, err := filepath.Glob(snap.CommonDataHomeDir())
+ if err != nil {
+ return nil, err
+ }
+
+ // then XDG_RUNTIME_DIRs for the users
+ foundXdg, err := filepath.Glob(snap.XdgRuntimeDirs())
+ if err != nil {
+ return nil, err
+ }
+ found = append(found, foundXdg...)
+
+ // then system data
+ found = append(found, snap.CommonDataDir())
+
+ return found, nil
+}
+
+// Copy all data for oldSnap to newSnap
+// (but never overwrite)
+func copySnapData(oldSnap, newSnap *snap.Info) (err error) {
+ oldDataDirs, err := snapDataDirs(oldSnap)
+ if err != nil {
+ return err
+ }
+
+ newSuffix := filepath.Base(newSnap.DataDir())
+ for _, oldDir := range oldDataDirs {
+ // replace the trailing "../$old-suffix" with the "../$new-suffix"
+ newDir := filepath.Join(filepath.Dir(oldDir), newSuffix)
+ if err := copySnapDataDirectory(oldDir, newDir); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// trashPath returns the trash path for the given path. This will
+// differ only in the last element.
+func trashPath(path string) string {
+ return path + ".old"
+}
+
+// trash moves path aside, if it exists. If the trash for the path
+// already exists and is not empty it will be removed first.
+func trash(path string) error {
+ trash := trashPath(path)
+ err := os.Rename(path, trash)
+ if err == nil {
+ return nil
+ }
+ // os.Rename says it always returns *os.LinkError. Be wary.
+ e, ok := err.(*os.LinkError)
+ if !ok {
+ return err
+ }
+
+ switch e.Err {
+ case unix.ENOENT:
+ // path does not exist (here we use that trashPath(path) and path differ only in the last element)
+ return nil
+ case unix.ENOTEMPTY, unix.EEXIST:
+ // path exists, but trash already exists and is non-empty
+ // (empirically always ENOTEMPTY but rename(2) says it can also be EEXIST)
+ // nuke the old trash and try again
+ if err := os.RemoveAll(trash); err != nil {
+ // well, that didn't work :-(
+ return err
+ }
+ return os.Rename(path, trash)
+ default:
+ // WAT
+ return err
+ }
+}
+
+// untrash moves the trash for path back in, if it exists.
+func untrash(path string) error {
+ err := os.Rename(trashPath(path), path)
+ if !os.IsNotExist(err) {
+ return err
+ }
+
+ return nil
+}
+
+// clearTrash removes the trash made for path, if it exists.
+func clearTrash(path string) error {
+ err := os.RemoveAll(trashPath(path))
+ if !os.IsNotExist(err) {
+ return err
+ }
+
+ return nil
+}
+
+// Lowlevel copy the snap data (but never override existing data)
+func copySnapDataDirectory(oldPath, newPath string) (err error) {
+ if _, err := os.Stat(oldPath); err == nil {
+ if err := trash(newPath); err != nil {
+ return err
+ }
+
+ if _, err := os.Stat(newPath); err != nil {
+ if err := osutil.CopyFile(oldPath, newPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync); err != nil {
+ return fmt.Errorf("cannot copy %q to %q: %v", oldPath, newPath, err)
+ }
+ }
+ } else if !os.IsNotExist(err) {
+ return err
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package backend
+
+// firstErr returns the first error of the given error list
+func firstErr(err ...error) error {
+ for _, e := range err {
+ if e != nil {
+ return e
+ }
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "golang.org/x/net/context"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+type fakeOp struct {
+ op string
+
+ name string
+ revno snap.Revision
+ sinfo snap.SideInfo
+ stype snap.Type
+ cand store.RefreshCandidate
+
+ old string
+
+ aliases []*backend.Alias
+ rmAliases []*backend.Alias
+}
+
+type fakeOps []fakeOp
+
+func (ops fakeOps) Ops() []string {
+ opsOps := make([]string, len(ops))
+ for i, op := range ops {
+ opsOps[i] = op.op
+ }
+
+ return opsOps
+}
+
+func (ops fakeOps) Count(op string) int {
+ n := 0
+ for i := range ops {
+ if ops[i].op == op {
+ n++
+ }
+ }
+ return n
+}
+
+func (ops fakeOps) First(op string) *fakeOp {
+ for i := range ops {
+ if ops[i].op == op {
+ return &ops[i]
+ }
+ }
+
+ return nil
+}
+
+type fakeDownload struct {
+ name string
+ macaroon string
+}
+
+type fakeStore struct {
+ downloads []fakeDownload
+ fakeBackend *fakeSnappyBackend
+ fakeCurrentProgress int
+ fakeTotalProgress int
+ state *state.State
+}
+
+func (f *fakeStore) pokeStateLock() {
+ // the store should be called without the state lock held. Try
+ // to acquire it.
+ f.state.Lock()
+ f.state.Unlock()
+}
+
+func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
+ f.pokeStateLock()
+
+ if spec.Revision.Unset() {
+ spec.Revision = snap.R(11)
+ if spec.Channel == "channel-for-7" {
+ spec.Revision.N = 7
+ }
+ }
+
+ confinement := snap.StrictConfinement
+ switch spec.Channel {
+ case "channel-for-devmode":
+ confinement = snap.DevModeConfinement
+ case "channel-for-classic":
+ confinement = snap.ClassicConfinement
+ }
+
+ info := &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: strings.Split(spec.Name, ".")[0],
+ Channel: spec.Channel,
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Revision: spec.Revision,
+ },
+ Version: spec.Name,
+ DownloadInfo: snap.DownloadInfo{
+ DownloadURL: "https://some-server.com/some/path.snap",
+ },
+ Confinement: confinement,
+ }
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap", name: spec.Name, revno: spec.Revision})
+
+ return info, nil
+}
+
+func (f *fakeStore) Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error) {
+ panic("Find called")
+}
+
+func (f *fakeStore) ListRefresh(cands []*store.RefreshCandidate, _ *auth.UserState) ([]*snap.Info, error) {
+ f.pokeStateLock()
+
+ if len(cands) == 0 {
+ return nil, nil
+ }
+ if len(cands) > 2 {
+ panic("ListRefresh unexpectedly called with more than two candidates")
+ }
+
+ var res []*snap.Info
+ for _, cand := range cands {
+ snapID := cand.SnapID
+
+ if snapID == "" || snapID == "other-snap-id" {
+ continue
+ }
+
+ if snapID == "fakestore-please-error-on-refresh" {
+ return nil, fmt.Errorf("failing as requested")
+ }
+
+ var name string
+ if snapID == "some-snap-id" {
+ name = "some-snap"
+ } else {
+ panic(fmt.Sprintf("ListRefresh: unknown snap-id: %s", snapID))
+ }
+
+ revno := snap.R(11)
+ confinement := snap.StrictConfinement
+ switch cand.Channel {
+ case "channel-for-7":
+ revno = snap.R(7)
+ case "channel-for-classic":
+ confinement = snap.ClassicConfinement
+ case "channel-for-devmode":
+ confinement = snap.DevModeConfinement
+ }
+
+ info := &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: name,
+ Channel: cand.Channel,
+ SnapID: cand.SnapID,
+ Revision: revno,
+ },
+ Version: name,
+ DownloadInfo: snap.DownloadInfo{
+ DownloadURL: "https://some-server.com/some/path.snap",
+ },
+ Confinement: confinement,
+ }
+
+ var hit snap.Revision
+ if cand.Revision != revno {
+ hit = revno
+ }
+ for _, blocked := range cand.Block {
+ if blocked == revno {
+ hit = snap.Revision{}
+ break
+ }
+ }
+
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-list-refresh", cand: *cand, revno: hit})
+
+ if !hit.Unset() {
+ res = append(res, info)
+ }
+ }
+
+ return res, nil
+}
+
+func (f *fakeStore) SuggestedCurrency() string {
+ f.pokeStateLock()
+
+ return "XTS"
+}
+
+func (f *fakeStore) Download(ctx context.Context, name, targetFn string, snapInfo *snap.DownloadInfo, pb progress.Meter, user *auth.UserState) error {
+ f.pokeStateLock()
+
+ var macaroon string
+ if user != nil {
+ macaroon = user.StoreMacaroon
+ }
+ f.downloads = append(f.downloads, fakeDownload{
+ macaroon: macaroon,
+ name: name,
+ })
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-download", name: name})
+
+ pb.SetTotal(float64(f.fakeTotalProgress))
+ pb.Set(float64(f.fakeCurrentProgress))
+
+ return nil
+}
+
+func (f *fakeStore) Buy(options *store.BuyOptions, user *auth.UserState) (*store.BuyResult, error) {
+ panic("Never expected fakeStore.Buy to be called")
+}
+
+func (f *fakeStore) ReadyToBuy(user *auth.UserState) error {
+ panic("Never expected fakeStore.ReadyToBuy to be called")
+}
+
+func (f *fakeStore) Assertion(*asserts.AssertionType, []string, *auth.UserState) (asserts.Assertion, error) {
+ panic("Never expected fakeStore.Assertion to be called")
+}
+
+func (f *fakeStore) Sections(user *auth.UserState) ([]string, error) {
+ panic("Sections called")
+}
+
+type fakeSnappyBackend struct {
+ ops fakeOps
+
+ linkSnapFailTrigger string
+ copySnapDataFailTrigger string
+}
+
+func (f *fakeSnappyBackend) OpenSnapFile(snapFilePath string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ op := fakeOp{
+ op: "open-snap-file",
+ name: snapFilePath,
+ }
+
+ if si != nil {
+ op.sinfo = *si
+ }
+
+ f.ops = append(f.ops, op)
+ return &snap.Info{Architectures: []string{"all"}}, nil, nil
+}
+
+func (f *fakeSnappyBackend) SetupSnap(snapFilePath string, si *snap.SideInfo, p progress.Meter) error {
+ p.Notify("setup-snap")
+ revno := snap.R(0)
+ if si != nil {
+ revno = si.Revision
+ }
+ f.ops = append(f.ops, fakeOp{
+ op: "setup-snap",
+ name: snapFilePath,
+ revno: revno,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
+ if name == "borken" {
+ return nil, errors.New(`cannot read info for "borken" snap`)
+ }
+ // naive emulation for now, always works
+ info := &snap.Info{SuggestedName: name, SideInfo: *si}
+ info.Type = snap.TypeApp
+ if name == "gadget" {
+ info.Type = snap.TypeGadget
+ }
+ if name == "core" {
+ info.Type = snap.TypeOS
+ }
+ if name == "alias-snap" {
+ var err error
+ info, err = snap.InfoFromSnapYaml([]byte(`name: alias-snap
+apps:
+ cmd1:
+ aliases: [alias1, alias1.cmd1]
+ cmd2:
+ aliases: [alias2]
+ cmd3:
+ aliases: [alias3]
+ cmd4:
+ aliases: [alias4]
+ cmd5:
+ aliases: [alias5]
+`))
+ if err != nil {
+ panic(err)
+ }
+ info.SideInfo = *si
+ }
+ return info, nil
+}
+
+func (f *fakeSnappyBackend) ClearTrashedData(si *snap.Info) {
+ f.ops = append(f.ops, fakeOp{
+ op: "cleanup-trash",
+ name: si.Name(),
+ revno: si.Revision,
+ })
+}
+
+func (f *fakeSnappyBackend) StoreInfo(st *state.State, name, channel string, userID int, flags snapstate.Flags) (*snap.Info, error) {
+ return f.ReadInfo(name, &snap.SideInfo{
+ RealName: name,
+ })
+}
+
+func (f *fakeSnappyBackend) CopySnapData(newInfo, oldInfo *snap.Info, p progress.Meter) error {
+ p.Notify("copy-data")
+ old := "<no-old>"
+ if oldInfo != nil {
+ old = oldInfo.MountDir()
+ }
+
+ if newInfo.MountDir() == f.copySnapDataFailTrigger {
+ f.ops = append(f.ops, fakeOp{
+ op: "copy-data.failed",
+ name: newInfo.MountDir(),
+ old: old,
+ })
+ return errors.New("fail")
+ }
+
+ f.ops = append(f.ops, fakeOp{
+ op: "copy-data",
+ name: newInfo.MountDir(),
+ old: old,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) LinkSnap(info *snap.Info) error {
+ if info.MountDir() == f.linkSnapFailTrigger {
+ f.ops = append(f.ops, fakeOp{
+ op: "link-snap.failed",
+ name: info.MountDir(),
+ })
+ return errors.New("fail")
+ }
+
+ f.ops = append(f.ops, fakeOp{
+ op: "link-snap",
+ name: info.MountDir(),
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) StartSnapServices(info *snap.Info, meter progress.Meter) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "start-snap-services",
+ name: info.MountDir(),
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) StopSnapServices(info *snap.Info, meter progress.Meter) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "stop-snap-services",
+ name: info.MountDir(),
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, p progress.Meter) error {
+ p.Notify("setup-snap")
+ f.ops = append(f.ops, fakeOp{
+ op: "undo-setup-snap",
+ name: s.MountDir(),
+ stype: typ,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, p progress.Meter) error {
+ p.Notify("undo-copy-data")
+ old := "<no-old>"
+ if oldInfo != nil {
+ old = oldInfo.MountDir()
+ }
+ f.ops = append(f.ops, fakeOp{
+ op: "undo-copy-snap-data",
+ name: newInfo.MountDir(),
+ old: old,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) UnlinkSnap(info *snap.Info, meter progress.Meter) error {
+ meter.Notify("unlink")
+ f.ops = append(f.ops, fakeOp{
+ op: "unlink-snap",
+ name: info.MountDir(),
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error {
+ meter.Notify("remove-snap-files")
+ f.ops = append(f.ops, fakeOp{
+ op: "remove-snap-files",
+ name: s.MountDir(),
+ stype: typ,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) RemoveSnapData(info *snap.Info) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "remove-snap-data",
+ name: info.MountDir(),
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) RemoveSnapCommonData(info *snap.Info) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "remove-snap-common-data",
+ name: info.MountDir(),
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) DiscardSnapNamespace(snapName string) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "discard-namespace",
+ name: snapName,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) Candidate(sideInfo *snap.SideInfo) {
+ var sinfo snap.SideInfo
+ if sideInfo != nil {
+ sinfo = *sideInfo
+ }
+ f.ops = append(f.ops, fakeOp{
+ op: "candidate",
+ sinfo: sinfo,
+ })
+}
+
+func (f *fakeSnappyBackend) CurrentInfo(curInfo *snap.Info) {
+ old := "<no-current>"
+ if curInfo != nil {
+ old = curInfo.MountDir()
+ }
+ f.ops = append(f.ops, fakeOp{
+ op: "current",
+ old: old,
+ })
+}
+
+func (f *fakeSnappyBackend) ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) {
+ f.ops = append(f.ops, fakeOp{
+ op: kind + ":" + status.String(),
+ name: snapsup.Name(),
+ revno: snapsup.Revision(),
+ })
+}
+
+func (f *fakeSnappyBackend) MatchingAliases(aliases []*backend.Alias) ([]*backend.Alias, error) {
+ f.ops = append(f.ops, fakeOp{
+ op: "matching-aliases",
+ aliases: aliases,
+ })
+ return aliases, nil
+}
+
+func (f *fakeSnappyBackend) MissingAliases(aliases []*backend.Alias) ([]*backend.Alias, error) {
+ f.ops = append(f.ops, fakeOp{
+ op: "missing-aliases",
+ aliases: aliases,
+ })
+ return aliases, nil
+}
+
+func (f *fakeSnappyBackend) UpdateAliases(add []*backend.Alias, remove []*backend.Alias) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "update-aliases",
+ aliases: add,
+ rmAliases: remove,
+ })
+ return nil
+}
+
+func (f *fakeSnappyBackend) RemoveSnapAliases(snapName string) error {
+ f.ops = append(f.ops, fakeOp{
+ op: "remove-snap-aliases",
+ name: snapName,
+ })
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+func nameAndRevnoFromSnap(sn string) (string, snap.Revision, error) {
+ l := strings.Split(sn, "_")
+ if len(l) < 2 {
+ return "", snap.Revision{}, fmt.Errorf("input %q has invalid format (not enough '_')", sn)
+ }
+ name := l[0]
+ revnoNSuffix := l[1]
+ rev, err := snap.ParseRevision(strings.Split(revnoNSuffix, ".snap")[0])
+ if err != nil {
+ return "", snap.Revision{}, err
+ }
+ return name, rev, nil
+}
+
+// UpdateBootRevisions synchronizes the active kernel and OS snap versions
+// with the versions that actually booted. This is needed because a
+// system may install "os=v2" but that fails to boot. The bootloader
+// fallback logic will revert to "os=v1" but on the filesystem snappy
+// still has the "active" version set to "v2" which is
+// misleading. This code will check what kernel/os booted and set
+// those versions active.To do this it creates a Change and kicks
+// start it directly.
+func UpdateBootRevisions(st *state.State) error {
+ const errorPrefix = "cannot update revisions after boot changes: "
+
+ if release.OnClassic {
+ return nil
+ }
+
+ bootloader, err := partition.FindBootloader()
+ if err != nil {
+ return fmt.Errorf(errorPrefix+"%s", err)
+ }
+
+ m, err := bootloader.GetBootVars("snap_kernel", "snap_core")
+ if err != nil {
+ return fmt.Errorf(errorPrefix+"%s", err)
+ }
+
+ var tsAll []*state.TaskSet
+ for _, snapNameAndRevno := range []string{m["snap_kernel"], m["snap_core"]} {
+ name, rev, err := nameAndRevnoFromSnap(snapNameAndRevno)
+ if err != nil {
+ logger.Noticef("cannot parse %q: %s", snapNameAndRevno, err)
+ continue
+ }
+ info, err := CurrentInfo(st, name)
+ if err != nil {
+ logger.Noticef("cannot get info for %q: %s", name, err)
+ continue
+ }
+ if rev != info.SideInfo.Revision {
+ // FIXME: check that there is no task
+ // for this already in progress
+ ts, err := RevertToRevision(st, name, rev, Flags{})
+ if err != nil {
+ return err
+ }
+ tsAll = append(tsAll, ts)
+ }
+ }
+
+ if len(tsAll) == 0 {
+ return nil
+ }
+
+ msg := fmt.Sprintf("Update kernel and core snap revisions")
+ chg := st.NewChange("update-revisions", msg)
+ for _, ts := range tsAll {
+ chg.AddAll(ts)
+ }
+ st.EnsureBefore(0)
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+// test the boot releated code
+
+import (
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/partition"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type bootedSuite struct {
+ bootloader *boottest.MockBootloader
+
+ state *state.State
+ snapmgr *snapstate.SnapManager
+ fakeBackend *fakeSnappyBackend
+}
+
+var _ = Suite(&bootedSuite{})
+
+func (bs *bootedSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+
+ // booted is not running on classic
+ release.MockOnClassic(false)
+
+ bs.bootloader = boottest.NewMockBootloader("mock", c.MkDir())
+ bs.bootloader.BootVars["snap_core"] = "core_2.snap"
+ bs.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_2.snap"
+ partition.ForceBootloader(bs.bootloader)
+
+ bs.fakeBackend = &fakeSnappyBackend{}
+ bs.state = state.New(nil)
+ bs.snapmgr, err = snapstate.Manager(bs.state)
+ c.Assert(err, IsNil)
+ bs.snapmgr.AddForeignTaskHandlers(bs.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(bs.snapmgr, bs.fakeBackend)
+ snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) {
+ return nil, nil
+ }
+}
+
+func (bs *bootedSuite) TearDownTest(c *C) {
+ snapstate.AutoAliases = nil
+ release.MockOnClassic(true)
+ dirs.SetRootDir("")
+ partition.ForceBootloader(nil)
+}
+
+var osSI1 = &snap.SideInfo{RealName: "core", Revision: snap.R(1)}
+var osSI2 = &snap.SideInfo{RealName: "core", Revision: snap.R(2)}
+var kernelSI1 = &snap.SideInfo{RealName: "canonical-pc-linux", Revision: snap.R(1)}
+var kernelSI2 = &snap.SideInfo{RealName: "canonical-pc-linux", Revision: snap.R(2)}
+
+func (bs *bootedSuite) settle() {
+ for i := 0; i < 50; i++ {
+ bs.snapmgr.Ensure()
+ bs.snapmgr.Wait()
+ }
+}
+
+func (bs *bootedSuite) makeInstalledKernelOS(c *C, st *state.State) {
+ snaptest.MockSnap(c, "name: core\ntype: os\nversion: 1", "", osSI1)
+ snaptest.MockSnap(c, "name: core\ntype: os\nversion: 2", "", osSI2)
+ snapstate.Set(st, "core", &snapstate.SnapState{
+ SnapType: "os",
+ Active: true,
+ Sequence: []*snap.SideInfo{osSI1, osSI2},
+ Current: snap.R(2),
+ })
+
+ snaptest.MockSnap(c, "name: canonical-pc-linux\ntype: os\nversion: 1", "", kernelSI1)
+ snaptest.MockSnap(c, "name: canonical-pc-linux\ntype: os\nversion: 2", "", kernelSI2)
+ snapstate.Set(st, "canonical-pc-linux", &snapstate.SnapState{
+ SnapType: "kernel",
+ Active: true,
+ Sequence: []*snap.SideInfo{kernelSI1, kernelSI2},
+ Current: snap.R(2),
+ })
+
+}
+
+func (bs *bootedSuite) TestUpdateBootRevisionsOSSimple(c *C) {
+ st := bs.state
+ st.Lock()
+ defer st.Unlock()
+
+ bs.makeInstalledKernelOS(c, st)
+
+ bs.bootloader.BootVars["snap_core"] = "core_1.snap"
+ err := snapstate.UpdateBootRevisions(st)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ bs.settle()
+ st.Lock()
+
+ c.Assert(st.Changes(), HasLen, 1)
+ chg := st.Changes()[0]
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Kind(), Equals, "update-revisions")
+ c.Assert(chg.IsReady(), Equals, true)
+
+ // core "current" got reverted but canonical-pc-linux did not
+ var snapst snapstate.SnapState
+ err = snapstate.Get(st, "core", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(1))
+ c.Assert(snapst.Active, Equals, true)
+
+ err = snapstate.Get(st, "canonical-pc-linux", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(2))
+ c.Assert(snapst.Active, Equals, true)
+}
+
+func (bs *bootedSuite) TestUpdateBootRevisionsKernelSimple(c *C) {
+ st := bs.state
+ st.Lock()
+ defer st.Unlock()
+
+ bs.makeInstalledKernelOS(c, st)
+
+ bs.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_1.snap"
+ err := snapstate.UpdateBootRevisions(st)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ bs.settle()
+ st.Lock()
+
+ c.Assert(st.Changes(), HasLen, 1)
+ chg := st.Changes()[0]
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Kind(), Equals, "update-revisions")
+ c.Assert(chg.IsReady(), Equals, true)
+
+ // canonical-pc-linux "current" got reverted but core did not
+ var snapst snapstate.SnapState
+ err = snapstate.Get(st, "canonical-pc-linux", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(1))
+ c.Assert(snapst.Active, Equals, true)
+
+ err = snapstate.Get(st, "core", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(2))
+ c.Assert(snapst.Active, Equals, true)
+}
+
+func (bs *bootedSuite) TestUpdateBootRevisionsKernelErrorsEarly(c *C) {
+ st := bs.state
+ st.Lock()
+ defer st.Unlock()
+
+ bs.makeInstalledKernelOS(c, st)
+
+ bs.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_99.snap"
+ err := snapstate.UpdateBootRevisions(st)
+ c.Assert(err, ErrorMatches, `cannot find revision 99 for snap "canonical-pc-linux"`)
+}
+
+func (bs *bootedSuite) TestUpdateBootRevisionsOSErrorsEarly(c *C) {
+ st := bs.state
+ st.Lock()
+ defer st.Unlock()
+
+ bs.makeInstalledKernelOS(c, st)
+
+ bs.bootloader.BootVars["snap_core"] = "core_99.snap"
+ err := snapstate.UpdateBootRevisions(st)
+ c.Assert(err, ErrorMatches, `cannot find revision 99 for snap "core"`)
+}
+
+func (bs *bootedSuite) TestUpdateBootRevisionsOSErrorsLate(c *C) {
+ st := bs.state
+ st.Lock()
+ defer st.Unlock()
+
+ // put core into the state but add no files on disk
+ // will break in the tasks
+ snapstate.Set(st, "core", &snapstate.SnapState{
+ SnapType: "os",
+ Active: true,
+ Sequence: []*snap.SideInfo{osSI1, osSI2},
+ Current: snap.R(2),
+ })
+ bs.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/core/1")
+
+ bs.bootloader.BootVars["snap_kernel"] = "core_1.snap"
+ err := snapstate.UpdateBootRevisions(st)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ bs.settle()
+ st.Lock()
+
+ c.Assert(st.Changes(), HasLen, 1)
+ chg := st.Changes()[0]
+ c.Assert(chg.Kind(), Equals, "update-revisions")
+ c.Assert(chg.IsReady(), Equals, true)
+ c.Assert(chg.Err(), ErrorMatches, `(?ms).*Make snap "core" \(1\) available to the system \(fail\).*`)
+}
+
+func (bs *bootedSuite) TestNameAndRevnoFromSnapValid(c *C) {
+ name, revno, err := snapstate.NameAndRevnoFromSnap("foo_2.snap")
+ c.Assert(err, IsNil)
+ c.Assert(name, Equals, "foo")
+ c.Assert(revno, Equals, snap.R(2))
+}
+
+func (bs *bootedSuite) TestNameAndRevnoFromSnapInvalidFormat(c *C) {
+ _, _, err := snapstate.NameAndRevnoFromSnap("invalid")
+ c.Assert(err, ErrorMatches, `input "invalid" has invalid format \(not enough '_'\)`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+// featureSet contains the flag values that can be listed in assumes entries
+// that this ubuntu-core actually provides.
+var featureSet = map[string]bool{
+ // Support for common data directory across revisions of a snap.
+ "common-data-dir": true,
+ // Support for the "Environment:" feature in snap.yaml
+ "snap-env": true,
+}
+
+func checkAssumes(si *snap.Info) error {
+ missing := ([]string)(nil)
+ for _, flag := range si.Assumes {
+ if strings.HasPrefix(flag, "snapd") && checkVersion(flag[5:]) {
+ continue
+ }
+ if !featureSet[flag] {
+ missing = append(missing, flag)
+ }
+ }
+ if len(missing) > 0 {
+ hint := "try to refresh the core snap"
+ if release.OnClassic {
+ hint = "try to update snapd and refresh the core snap"
+ }
+ return fmt.Errorf("snap %q assumes unsupported features: %s (%s)", si.Name(), strings.Join(missing, ", "), hint)
+ }
+ return nil
+}
+
+var versionExp = regexp.MustCompile(`^([1-9][0-9]*)(?:\.([0-9]+)(?:\.([0-9]+))?)?`)
+
+func checkVersion(version string) bool {
+ req := versionExp.FindStringSubmatch(version)
+ if req == nil || req[0] != version {
+ return false
+ }
+
+ if cmd.Version == "unknown" {
+ return true // Development tree.
+ }
+
+ cur := versionExp.FindStringSubmatch(cmd.Version)
+ if cur == nil {
+ return false
+ }
+
+ for i := 1; i < len(req); i++ {
+ if req[i] == "" {
+ return true
+ }
+ if cur[i] == "" {
+ return false
+ }
+ reqN, err1 := strconv.Atoi(req[i])
+ curN, err2 := strconv.Atoi(cur[i])
+ if err1 != nil || err2 != nil {
+ panic("internal error: version regexp is broken")
+ }
+ if curN != reqN {
+ return curN > reqN
+ }
+ }
+
+ return true
+}
+
+var openSnapFile = backend.OpenSnapFile
+
+// checkSnap ensures that the snap can be installed.
+func checkSnap(st *state.State, snapFilePath string, si *snap.SideInfo, curInfo *snap.Info, flags Flags) error {
+ // This assumes that the snap was already verified or --dangerous was used.
+
+ s, _, err := openSnapFile(snapFilePath, si)
+ if err != nil {
+ return err
+ }
+
+ if s.NeedsDevMode() && !flags.DevModeAllowed() {
+ return fmt.Errorf("snap %q requires devmode or confinement override", s.Name())
+ }
+ if s.NeedsClassic() {
+ if !release.OnClassic {
+ return fmt.Errorf("snap %q requires classic confinement which is only available on classic systems", s.Name())
+ }
+ if !flags.Classic {
+ return fmt.Errorf("snap %q requires consent to use classic confinement", s.Name())
+ }
+ }
+
+ // verify we have a valid architecture
+ if !arch.IsSupportedArchitecture(s.Architectures) {
+ return fmt.Errorf("snap %q supported architectures (%s) are incompatible with this system (%s)", s.Name(), strings.Join(s.Architectures, ", "), arch.UbuntuArchitecture())
+ }
+
+ // check assumes
+ err = checkAssumes(s)
+ if err != nil {
+ return err
+ }
+
+ st.Lock()
+ defer st.Unlock()
+
+ for _, check := range checkSnapCallbacks {
+ err := check(st, s, curInfo, flags)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// CheckSnapCallback defines callbacks for checking a snap for installation or refresh.
+type CheckSnapCallback func(st *state.State, snap, curSnap *snap.Info, flags Flags) error
+
+var checkSnapCallbacks []CheckSnapCallback
+
+// AddCheckSnapCallback installs a callback to check a snap for installation or refresh.
+func AddCheckSnapCallback(check CheckSnapCallback) {
+ checkSnapCallbacks = append(checkSnapCallbacks, check)
+}
+
+func MockCheckSnapCallbacks(checks []CheckSnapCallback) (restore func()) {
+ prev := checkSnapCallbacks
+ checkSnapCallbacks = checks
+ return func() {
+ checkSnapCallbacks = prev
+ }
+}
+
+func checkCoreName(st *state.State, snapInfo, curInfo *snap.Info, flags Flags) error {
+ if snapInfo.Type != snap.TypeOS {
+ // not a relevant check
+ return nil
+ }
+ if curInfo != nil {
+ // already one of these installed
+ return nil
+ }
+ core, err := CoreInfo(st)
+ if err == state.ErrNoState {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+
+ if core.Name() != snapInfo.Name() {
+ return fmt.Errorf("cannot install core snap %q when core snap %q is already present", snapInfo.Name(), core.Name())
+ }
+
+ return nil
+}
+
+func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags Flags) error {
+ kind := ""
+ var currentInfo func(*state.State) (*snap.Info, error)
+ switch snapInfo.Type {
+ case snap.TypeGadget:
+ kind = "gadget"
+ currentInfo = GadgetInfo
+ case snap.TypeKernel:
+ kind = "kernel"
+ currentInfo = KernelInfo
+ default:
+ // not a relevant check
+ return nil
+ }
+
+ if release.OnClassic {
+ // for the time being
+ return fmt.Errorf("cannot install a %s snap on classic", kind)
+ }
+
+ currentSnap, err := currentInfo(st)
+ // in firstboot we have no gadget/kernel yet - that is ok
+ // devicestate considers that case
+ if err == state.ErrNoState {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("cannot find original %s snap: %v", kind, err)
+ }
+
+ if currentSnap.SnapID != "" && snapInfo.SnapID != "" {
+ if currentSnap.SnapID == snapInfo.SnapID {
+ // same snap
+ return nil
+ }
+ return fmt.Errorf("cannot replace %s snap with a different one", kind)
+ }
+
+ if currentSnap.SnapID != "" && snapInfo.SnapID == "" {
+ return fmt.Errorf("cannot replace signed %s snap with an unasserted one", kind)
+ }
+
+ if currentSnap.Name() != snapInfo.Name() {
+ return fmt.Errorf("cannot replace %s snap with a different one", kind)
+ }
+
+ return nil
+}
+
+func init() {
+ AddCheckSnapCallback(checkCoreName)
+ AddCheckSnapCallback(checkGadgetOrKernel)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ "errors"
+ "fmt"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+)
+
+type checkSnapSuite struct {
+ st *state.State
+}
+
+var _ = Suite(&checkSnapSuite{})
+
+func (s *checkSnapSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ s.st = state.New(nil)
+}
+
+func (s *checkSnapSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *checkSnapSuite) TestCheckSnapErrorOnUnsupportedArchitecture(c *C) {
+ const yaml = `name: hello
+version: 1.10
+architectures:
+ - yadayada
+ - blahblah
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ c.Check(path, Equals, "snap-path")
+ c.Check(si, IsNil)
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{})
+
+ errorMsg := fmt.Sprintf(`snap "hello" supported architectures (yadayada, blahblah) are incompatible with this system (%s)`, arch.UbuntuArchitecture())
+ c.Assert(err.Error(), Equals, errorMsg)
+}
+
+var assumesTests = []struct {
+ version string
+ assumes string
+ classic bool
+ error string
+}{{
+ assumes: "[common-data-dir]",
+}, {
+ assumes: "[f1, f2]",
+ error: `snap "foo" assumes unsupported features: f1, f2 \(try to refresh the core snap\)`,
+}, {
+ assumes: "[f1, f2]",
+ classic: true,
+ error: `snap "foo" assumes unsupported features: f1, f2 \(try to update snapd and refresh the core snap\)`,
+}, {
+ assumes: "[snapd2.15]",
+ version: "unknown",
+}, {
+ assumes: "[snapdnono]",
+ version: "unknown",
+ error: `.* unsupported features: snapdnono .*`,
+}, {
+ assumes: "[snapd2.15nono]",
+ version: "unknown",
+ error: `.* unsupported features: snapd2.15nono .*`,
+}, {
+ assumes: "[snapd2.15]",
+ version: "2.15",
+}, {
+ assumes: "[snapd2.15]",
+ version: "2.15.1",
+}, {
+ assumes: "[snapd2.15]",
+ version: "2.15+git",
+}, {
+ assumes: "[snapd2.15]",
+ version: "2.16",
+}, {
+ assumes: "[snapd2.15.1]",
+ version: "2.16",
+}, {
+ assumes: "[snapd2.15.2]",
+ version: "2.16.1",
+}, {
+ assumes: "[snapd3]",
+ version: "3.1",
+}, {
+ assumes: "[snapd2.16]",
+ version: "2.15",
+ error: `.* unsupported features: snapd2\.16 .*`,
+}, {
+ assumes: "[snapd2.15.1]",
+ version: "2.15",
+ error: `.* unsupported features: snapd2\.15\.1 .*`,
+}, {
+ assumes: "[snapd2.15.1]",
+ version: "2.15.0",
+ error: `.* unsupported features: snapd2\.15\.1 .*`,
+}}
+
+func (s *checkSnapSuite) TestCheckSnapAssumes(c *C) {
+ restore := cmd.MockVersion("2.15")
+ defer restore()
+
+ restore = release.MockOnClassic(false)
+ defer restore()
+
+ for _, test := range assumesTests {
+ cmd.Version = test.version
+ if cmd.Version == "" {
+ cmd.Version = "2.15"
+ }
+ release.OnClassic = test.classic
+
+ yaml := fmt.Sprintf("name: foo\nversion: 1.0\nassumes: %s\n", test.assumes)
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+ err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{})
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ } else {
+ c.Assert(err, IsNil)
+ }
+ }
+}
+
+func (s *checkSnapSuite) TestCheckSnapCheckCallbackOK(c *C) {
+ const yaml = `name: foo
+version: 1.0`
+
+ si := &snap.SideInfo{
+ SnapID: "snap-id",
+ }
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ info := snaptest.MockInfo(c, yaml, si)
+ return info, nil, nil
+ }
+ r1 := snapstate.MockOpenSnapFile(openSnapFile)
+ defer r1()
+
+ checkCbCalled := false
+ checkCb := func(st *state.State, s, cur *snap.Info, flags snapstate.Flags) error {
+ c.Assert(s.Name(), Equals, "foo")
+ c.Assert(s.SnapID, Equals, "snap-id")
+ checkCbCalled = true
+ return nil
+ }
+ r2 := snapstate.MockCheckSnapCallbacks([]snapstate.CheckSnapCallback{checkCb})
+ defer r2()
+
+ err := snapstate.CheckSnap(s.st, "snap-path", si, nil, snapstate.Flags{})
+ c.Check(err, IsNil)
+
+ c.Check(checkCbCalled, Equals, true)
+}
+
+func (s *checkSnapSuite) TestCheckSnapCheckCallbackFail(c *C) {
+ const yaml = `name: foo
+version: 1.0`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ fail := errors.New("bad snap")
+ checkCb := func(st *state.State, s, cur *snap.Info, flags snapstate.Flags) error {
+ return fail
+ }
+ r2 := snapstate.MockCheckSnapCallbacks(nil)
+ defer r2()
+ snapstate.AddCheckSnapCallback(checkCb)
+
+ err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{})
+ c.Check(err, Equals, fail)
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetUpdate(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "gadget", Revision: snap.R(2), SnapID: "gadget-id"}
+ snaptest.MockSnap(c, `
+name: gadget
+type: gadget
+version: 1
+`, "", si)
+ snapstate.Set(st, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: gadget
+type: gadget
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ info.SnapID = "gadget-id"
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, IsNil)
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetUpdateLocal(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "gadget", Revision: snap.R(2)}
+ snaptest.MockSnap(c, `
+name: gadget
+type: gadget
+version: 1
+`, "", si)
+ snapstate.Set(st, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: gadget
+type: gadget
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ // no SnapID => local!
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, IsNil)
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetUpdateToUnassertedProhibited(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "gadget", Revision: snap.R(2), SnapID: "gadget-id"}
+ snaptest.MockSnap(c, `
+name: gadget
+type: gadget
+version: 1
+`, "", si)
+ snapstate.Set(st, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: gadget
+type: gadget
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, ErrorMatches, `cannot replace signed gadget snap with an unasserted one`)
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetAdditionProhibited(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "gadget", Revision: snap.R(2)}
+ snaptest.MockSnap(c, `
+name: gadget
+type: gadget
+version: 1
+`, "", si)
+ snapstate.Set(st, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: zgadget
+type: gadget
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, ErrorMatches, "cannot replace gadget snap with a different one")
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetAdditionProhibitedBySnapID(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "gadget", Revision: snap.R(2), SnapID: "gadget-id"}
+ snaptest.MockSnap(c, `
+name: gadget
+type: gadget
+version: 1
+`, "", si)
+ snapstate.Set(st, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: zgadget
+type: gadget
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ info.SnapID = "zgadget-id"
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, ErrorMatches, "cannot replace gadget snap with a different one")
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetNoPrior(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+ st.Set("seeded", true)
+
+ const yaml = `name: gadget
+type: gadget
+version: 1
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, IsNil)
+}
+
+func (s *checkSnapSuite) TestCheckSnapGadgetCannotBeInstalledOnClassic(c *C) {
+ reset := release.MockOnClassic(true)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ const yaml = `name: gadget
+type: gadget
+version: 1
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, ErrorMatches, "cannot install a gadget snap on classic")
+}
+
+func (s *checkSnapSuite) TestCheckSnapErrorOnDevModeDisallowed(c *C) {
+ const yaml = `name: hello
+version: 1.10
+confinement: devmode
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ c.Check(path, Equals, "snap-path")
+ c.Check(si, IsNil)
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{})
+
+ c.Assert(err, ErrorMatches, ".* requires devmode or confinement override")
+}
+
+func (s *checkSnapSuite) TestCheckSnapErrorOnClassicDisallowed(c *C) {
+ const yaml = `name: hello
+version: 1.10
+confinement: classic
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ c.Check(path, Equals, "snap-path")
+ c.Check(si, IsNil)
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ restore = release.MockOnClassic(true)
+ defer restore()
+
+ err = snapstate.CheckSnap(s.st, "snap-path", nil, nil, snapstate.Flags{})
+
+ c.Assert(err, ErrorMatches, ".* requires consent to use classic confinement")
+}
+
+func (s *checkSnapSuite) TestCheckSnapKernelUpdate(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "kernel", Revision: snap.R(2), SnapID: "kernel-id"}
+ snaptest.MockSnap(c, `
+name: kernel
+type: kernel
+version: 1
+`, "", si)
+ snapstate.Set(st, "kernel", &snapstate.SnapState{
+ SnapType: "kernel",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: kernel
+type: kernel
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ info.SnapID = "kernel-id"
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, IsNil)
+}
+
+func (s *checkSnapSuite) TestCheckSnapKernelAdditionProhibitedBySnapID(c *C) {
+ reset := release.MockOnClassic(false)
+ defer reset()
+
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "kernel", Revision: snap.R(2), SnapID: "kernel-id"}
+ snaptest.MockSnap(c, `
+name: kernel
+type: kernel
+version: 1
+`, "", si)
+ snapstate.Set(st, "kernel", &snapstate.SnapState{
+ SnapType: "kernel",
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ })
+
+ const yaml = `name: zkernel
+type: kernel
+version: 2
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ info.SnapID = "zkernel-id"
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, nil, nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
+ st.Lock()
+ c.Check(err, ErrorMatches, "cannot replace kernel snap with a different one")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type discardSnapSuite struct {
+ state *state.State
+ snapmgr *snapstate.SnapManager
+
+ fakeBackend *fakeSnappyBackend
+
+ reset func()
+}
+
+var _ = Suite(&discardSnapSuite{})
+
+func (s *discardSnapSuite) SetUpTest(c *C) {
+ s.fakeBackend = &fakeSnappyBackend{}
+ s.state = state.New(nil)
+
+ var err error
+ s.snapmgr, err = snapstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend)
+
+ s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo)
+}
+
+func (s *discardSnapSuite) TearDownTest(c *C) {
+ s.reset()
+}
+
+func (s *discardSnapSuite) TestDoDiscardSnapSuccess(c *C) {
+ s.state.Lock()
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", Revision: snap.R(3)},
+ {RealName: "foo", Revision: snap.R(33)},
+ },
+ Current: snap.R(33),
+ SnapType: "app",
+ })
+ t := s.state.NewTask("discard-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(33),
+ },
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Check(snapst.Sequence, HasLen, 1)
+ c.Check(snapst.Current, Equals, snap.R(3))
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
+
+func (s *discardSnapSuite) TestDoDiscardSnapToEmpty(c *C) {
+ s.state.Lock()
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", Revision: snap.R(3)},
+ },
+ Current: snap.R(3),
+ SnapType: "app",
+ })
+ t := s.state.NewTask("discard-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(33),
+ },
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+}
+
+func (s *discardSnapSuite) TestDoDiscardSnapErrorsForActive(c *C) {
+ s.state.Lock()
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", Revision: snap.R(3)},
+ },
+ Current: snap.R(3),
+ Active: true,
+ SnapType: "app",
+ })
+ t := s.state.NewTask("discard-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(3),
+ },
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+ c.Check(chg.Err(), ErrorMatches, `(?s).*internal error: cannot discard snap "foo": still active.*`)
+}
+
+func (s *discardSnapSuite) TestDoDiscardSnapNoErrorsForActive(c *C) {
+ s.state.Lock()
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", Revision: snap.R(3)},
+ {RealName: "foo", Revision: snap.R(33)},
+ },
+ Current: snap.R(33),
+ Active: true,
+ SnapType: "app",
+ })
+ t := s.state.NewTask("discard-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(3),
+ },
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Err(), IsNil)
+ c.Check(snapst.Sequence, HasLen, 1)
+ c.Check(snapst.Current, Equals, snap.R(33))
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type downloadSnapSuite struct {
+ state *state.State
+ snapmgr *snapstate.SnapManager
+ fakeStore *fakeStore
+
+ fakeBackend *fakeSnappyBackend
+
+ reset func()
+}
+
+var _ = Suite(&downloadSnapSuite{})
+
+func (s *downloadSnapSuite) SetUpTest(c *C) {
+ s.fakeBackend = &fakeSnappyBackend{}
+ s.state = state.New(nil)
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.fakeStore = &fakeStore{
+ state: s.state,
+ fakeBackend: s.fakeBackend,
+ }
+ snapstate.ReplaceStore(s.state, s.fakeStore)
+
+ var err error
+ s.snapmgr, err = snapstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend)
+
+ s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo)
+}
+
+func (s *downloadSnapSuite) TearDownTest(c *C) {
+ s.reset()
+}
+
+func (s *downloadSnapSuite) TestDoDownloadSnapCompatbility(c *C) {
+ s.state.Lock()
+ t := s.state.NewTask("download-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ },
+ Channel: "some-channel",
+ // explicitly set to "nil", this ensures the compatibility
+ // code path in the task is hit and the store is queried
+ // in the task (instead of using the new
+ // SnapSetup.{SideInfo,DownloadInfo} that gets set in
+ // snapstate.{Install,Update} directely.
+ DownloadInfo: nil,
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ // the compat code called the store "Snap" endpoint
+ c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{
+ {
+ op: "storesvc-snap",
+ name: "foo",
+ revno: snap.R(11),
+ },
+ {
+ op: "storesvc-download",
+ name: "foo",
+ },
+ })
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var snapsup snapstate.SnapSetup
+ t.Get("snap-setup", &snapsup)
+ c.Check(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "foo",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Revision: snap.R(11),
+ Channel: "some-channel",
+ })
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
+
+func (s *downloadSnapSuite) TestDoDownloadSnapNormal(c *C) {
+ s.state.Lock()
+
+ si := &snap.SideInfo{
+ RealName: "foo",
+ SnapID: "mySnapID",
+ Revision: snap.R(11),
+ Channel: "my-channel",
+ }
+
+ // download, ensure the store does not query
+ t := s.state.NewTask("download-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ Channel: "some-channel",
+ SideInfo: si,
+ DownloadInfo: &snap.DownloadInfo{
+ DownloadURL: "http://some-url.com/snap",
+ },
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ // only the download endpoint of the store was hit
+ c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{
+ {
+ op: "storesvc-download",
+ name: "foo",
+ },
+ })
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var snapsup snapstate.SnapSetup
+ t.Get("snap-setup", &snapsup)
+ c.Check(snapsup.SideInfo, DeepEquals, si)
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
+
+func (s *downloadSnapSuite) TestDoUndoDownloadSnap(c *C) {
+ s.state.Lock()
+ si := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(33),
+ }
+ t := s.state.NewTask("download-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ DownloadInfo: &snap.DownloadInfo{
+ DownloadURL: "http://something.com/snap",
+ },
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // task was undone
+ c.Check(t.Status(), Equals, state.UndoneStatus)
+
+ // and nothing is in the state for "foo"
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "errors"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type ManagerBackend managerBackend
+
+func SetSnapManagerBackend(s *SnapManager, b ManagerBackend) {
+ s.backend = b
+}
+
+type ForeignTaskTracker interface {
+ ForeignTask(kind string, status state.Status, snapsup *SnapSetup)
+}
+
+// AddForeignTaskHandlers registers handlers for tasks handled outside of the snap manager.
+func (m *SnapManager) AddForeignTaskHandlers(tracker ForeignTaskTracker) {
+ // Add fake handlers for tasks handled by interfaces manager
+ fakeHandler := func(task *state.Task, _ *tomb.Tomb) error {
+ task.State().Lock()
+ kind := task.Kind()
+ status := task.Status()
+ snapsup, err := TaskSnapSetup(task)
+ task.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ tracker.ForeignTask(kind, status, snapsup)
+
+ return nil
+ }
+ m.runner.AddHandler("setup-profiles", fakeHandler, fakeHandler)
+ m.runner.AddHandler("remove-profiles", fakeHandler, fakeHandler)
+ m.runner.AddHandler("discard-conns", fakeHandler, fakeHandler)
+ m.runner.AddHandler("validate-snap", fakeHandler, nil)
+
+ // Add handler to test full aborting of changes
+ erroringHandler := func(task *state.Task, _ *tomb.Tomb) error {
+ return errors.New("error out")
+ }
+ m.runner.AddHandler("error-trigger", erroringHandler, nil)
+
+ m.runner.AddHandler("run-hook", func(task *state.Task, _ *tomb.Tomb) error {
+ return nil
+ }, nil)
+}
+
+// AddAdhocTaskHandlers registers handlers for ad hoc test handler
+func (m *SnapManager) AddAdhocTaskHandler(adhoc string, do, undo func(*state.Task, *tomb.Tomb) error) {
+ m.runner.AddHandler(adhoc, do, undo)
+}
+
+func MockReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, error)) (restore func()) {
+ old := readInfo
+ readInfo = mock
+ return func() { readInfo = old }
+}
+
+func MockOpenSnapFile(mock func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error)) (restore func()) {
+ prevOpenSnapFile := openSnapFile
+ openSnapFile = mock
+ return func() { openSnapFile = prevOpenSnapFile }
+}
+
+var (
+ CheckSnap = checkSnap
+ CanRemove = canRemove
+ CanDisable = canDisable
+ CachedStore = cachedStore
+ NameAndRevnoFromSnap = nameAndRevnoFromSnap
+)
+
+func PreviousSideInfo(snapst *SnapState) *snap.SideInfo {
+ return snapst.previousSideInfo()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+// Flags are used to pass additional flags to operations and to keep track of snap modes.
+type Flags struct {
+ // DevMode switches confinement to non-enforcing mode.
+ DevMode bool `json:"devmode,omitempty"`
+ // JailMode is set when the user has requested confinement
+ // always be enforcing, even if the snap requests otherwise.
+ JailMode bool `json:"jailmode,omitempty"`
+ // Classic is set when the user has consented to install a snap with
+ // classic confinement and the snap declares that confinement.
+ Classic bool `json:"classic,omitempty"`
+ // TryMode is set for snaps installed to try directly from a local directory.
+ TryMode bool `json:"trymode,omitempty"`
+
+ // Revert flags the SnapSetup as coming from a revert
+ Revert bool `json:"revert,omitempty"`
+
+ // RemoveSnapPath is used via InstallPath to flag that the file passed in is temporary and should be removed
+ RemoveSnapPath bool `json:"remove-snap-path,omitempty"`
+
+ // IgnoreValidation is set when the user requested as one-off
+ // to ignore refresh control validation.
+ IgnoreValidation bool `json:"ignore-validation,omitempty"`
+}
+
+// DevModeAllowed returns whether a snap can be installed with devmode confinement (either set or overridden)
+func (f Flags) DevModeAllowed() bool {
+ return f.DevMode || f.JailMode
+}
+
+// ForSnapSetup returns a copy of the Flags with the flags that we don't need in SnapSetup set to false (so they're not serialized)
+func (f Flags) ForSnapSetup() Flags {
+ f.IgnoreValidation = false
+ return f
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+type linkSnapSuite struct {
+ state *state.State
+ snapmgr *snapstate.SnapManager
+
+ fakeBackend *fakeSnappyBackend
+
+ stateBackend *witnessRestartReqStateBackend
+
+ reset func()
+}
+
+var _ = Suite(&linkSnapSuite{})
+
+type witnessRestartReqStateBackend struct {
+ restartRequested bool
+}
+
+func (b *witnessRestartReqStateBackend) Checkpoint([]byte) error {
+ return nil
+}
+
+func (b *witnessRestartReqStateBackend) RequestRestart(t state.RestartType) {
+ b.restartRequested = true
+}
+
+func (b *witnessRestartReqStateBackend) EnsureBefore(time.Duration) {}
+
+func (s *linkSnapSuite) SetUpTest(c *C) {
+ s.stateBackend = &witnessRestartReqStateBackend{}
+ s.fakeBackend = &fakeSnappyBackend{}
+ s.state = state.New(s.stateBackend)
+
+ var err error
+ s.snapmgr, err = snapstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend)
+
+ s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo)
+}
+
+func (s *linkSnapSuite) TearDownTest(c *C) {
+ s.reset()
+}
+
+func (s *linkSnapSuite) TestDoLinkSnapSuccess(c *C) {
+ s.state.Lock()
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(33),
+ },
+ Channel: "beta",
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+
+ typ, err := snapst.Type()
+ c.Check(err, IsNil)
+ c.Check(typ, Equals, snap.TypeApp)
+
+ c.Check(snapst.Active, Equals, true)
+ c.Check(snapst.Sequence, HasLen, 1)
+ c.Check(snapst.Current, Equals, snap.R(33))
+ c.Check(snapst.Channel, Equals, "beta")
+ c.Check(t.Status(), Equals, state.DoneStatus)
+ c.Check(s.stateBackend.restartRequested, Equals, false)
+}
+
+func (s *linkSnapSuite) TestDoUndoLinkSnap(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ si := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(33),
+ }
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ Channel: "beta",
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+ c.Check(t.Status(), Equals, state.UndoneStatus)
+}
+
+func (s *linkSnapSuite) TestDoLinkSnapTryToCleanupOnError(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ si := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(35),
+ }
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ Channel: "beta",
+ })
+
+ s.fakeBackend.linkSnapFailTrigger = "/snap/foo/35"
+ s.state.NewChange("dummy", "...").AddTask(t)
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+
+ // state as expected
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+
+ // tried to cleanup
+ c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{
+ {
+ op: "candidate",
+ sinfo: *si,
+ },
+ {
+ op: "link-snap.failed",
+ name: "/snap/foo/35",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/foo/35",
+ },
+ })
+}
+
+func (s *linkSnapSuite) TestDoLinkSnapSuccessCoreRestarts(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ s.state.Lock()
+ si := &snap.SideInfo{
+ RealName: "core",
+ Revision: snap.R(33),
+ }
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "core", &snapst)
+ c.Assert(err, IsNil)
+
+ typ, err := snapst.Type()
+ c.Check(err, IsNil)
+ c.Check(typ, Equals, snap.TypeOS)
+
+ c.Check(t.Status(), Equals, state.DoneStatus)
+ c.Check(s.stateBackend.restartRequested, Equals, true)
+ c.Check(t.Log(), HasLen, 1)
+ c.Check(t.Log()[0], Matches, `.*INFO Requested daemon restart\.`)
+}
+
+func (s *linkSnapSuite) TestDoUndoLinkSnapSequenceDidNotHaveCandidate(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ si1 := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(1),
+ }
+ si2 := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(2),
+ }
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si1},
+ Current: si1.Revision,
+ })
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si2,
+ Channel: "beta",
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Active, Equals, false)
+ c.Check(snapst.Sequence, HasLen, 1)
+ c.Check(snapst.Current, Equals, snap.R(1))
+ c.Check(t.Status(), Equals, state.UndoneStatus)
+}
+
+func (s *linkSnapSuite) TestDoUndoLinkSnapSequenceHadCandidate(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ si1 := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(1),
+ }
+ si2 := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(2),
+ }
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si1, si2},
+ Current: si2.Revision,
+ })
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si1,
+ Channel: "beta",
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Active, Equals, false)
+ c.Check(snapst.Sequence, HasLen, 2)
+ c.Check(snapst.Current, Equals, snap.R(2))
+ c.Check(t.Status(), Equals, state.UndoneStatus)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type mountSnapSuite struct {
+ state *state.State
+ snapmgr *snapstate.SnapManager
+
+ fakeBackend *fakeSnappyBackend
+
+ reset func()
+}
+
+var _ = Suite(&mountSnapSuite{})
+
+func (s *mountSnapSuite) SetUpTest(c *C) {
+ s.fakeBackend = &fakeSnappyBackend{}
+ s.state = state.New(nil)
+
+ var err error
+ s.snapmgr, err = snapstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend)
+
+ s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo)
+}
+
+func (s *mountSnapSuite) TearDownTest(c *C) {
+ s.reset()
+}
+
+func (s *mountSnapSuite) TestDoMountSnapDoesNotRemovesSnaps(c *C) {
+ v1 := "name: mock\nversion: 1.0\n"
+ testSnap := snaptest.MakeTestSnapWithFiles(c, v1, nil)
+
+ s.state.Lock()
+
+ t := s.state.NewTask("mount-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(33),
+ },
+ SnapPath: testSnap,
+ DownloadInfo: &snap.DownloadInfo{DownloadURL: "https://some"},
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ c.Assert(osutil.FileExists(testSnap), Equals, true)
+}
+
+func (s *mountSnapSuite) TestDoUndoMountSnap(c *C) {
+ v1 := "name: core\nversion: 1.0\n"
+ testSnap := snaptest.MakeTestSnapWithFiles(c, v1, nil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ si1 := &snap.SideInfo{
+ RealName: "core",
+ Revision: snap.R(1),
+ }
+ si2 := &snap.SideInfo{
+ RealName: "core",
+ Revision: snap.R(2),
+ }
+ snapstate.Set(s.state, "core", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si1},
+ Current: si1.Revision,
+ SnapType: "os",
+ })
+
+ t := s.state.NewTask("mount-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si2,
+ SnapPath: testSnap,
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(t)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+
+ for i := 0; i < 3; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ // ensure undo was called the right way
+ c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{
+ {
+ op: "current",
+ old: "/snap/core/1",
+ },
+ {
+ op: "setup-snap",
+ name: testSnap,
+ revno: snap.R(2),
+ },
+ {
+ op: "undo-setup-snap",
+ name: "/snap/core/2",
+ stype: "os",
+ },
+ })
+
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type prepareSnapSuite struct {
+ state *state.State
+ snapmgr *snapstate.SnapManager
+
+ fakeBackend *fakeSnappyBackend
+
+ reset func()
+}
+
+var _ = Suite(&prepareSnapSuite{})
+
+func (s *prepareSnapSuite) SetUpTest(c *C) {
+ s.fakeBackend = &fakeSnappyBackend{}
+ s.state = state.New(nil)
+
+ var err error
+ s.snapmgr, err = snapstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend)
+
+ s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo)
+}
+
+func (s *prepareSnapSuite) TearDownTest(c *C) {
+ s.reset()
+}
+
+func (s *prepareSnapSuite) TestDoPrepareSnapSimple(c *C) {
+ s.state.Lock()
+ t := s.state.NewTask("prepare-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "foo",
+ },
+ })
+ s.state.NewChange("dummy", "...").AddTask(t)
+
+ s.state.Unlock()
+
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var snapsup snapstate.SnapSetup
+ t.Get("snap-setup", &snapsup)
+ c.Check(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(-1),
+ })
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// TaskProgressAdapter adapts the progress.Meter to the task progress
+// until we have native install/update/remove.
+type TaskProgressAdapter struct {
+ task *state.Task
+ label string
+ total float64
+ current float64
+}
+
+// Start sets total
+func (t *TaskProgressAdapter) Start(label string, total float64) {
+ t.label = label
+ t.total = total
+}
+
+// Set sets the current progress
+func (t *TaskProgressAdapter) Set(current float64) {
+ t.task.State().Lock()
+ defer t.task.State().Unlock()
+ t.task.SetProgress(t.label, int(current), int(t.total))
+}
+
+// SetTotal sets tht maximum progress
+func (t *TaskProgressAdapter) SetTotal(total float64) {
+ t.total = total
+}
+
+// Finished set the progress to 100%
+func (t *TaskProgressAdapter) Finished() {
+ t.task.State().Lock()
+ defer t.task.State().Unlock()
+ t.task.SetProgress(t.label, int(t.total), int(t.total))
+}
+
+// Write sets the current write progress
+func (t *TaskProgressAdapter) Write(p []byte) (n int, err error) {
+ t.task.State().Lock()
+ defer t.task.State().Unlock()
+
+ t.current += float64(len(p))
+ t.task.SetProgress(t.label, int(t.current), int(t.total))
+ return len(p), nil
+}
+
+// Notify notifies
+func (t *TaskProgressAdapter) Notify(msg string) {
+ t.task.State().Lock()
+ defer t.task.State().Unlock()
+ t.task.Logf(msg)
+}
+
+// Spin does nothing
+func (t *TaskProgressAdapter) Spin(msg string) {
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate
+
+import (
+ "github.com/snapcore/snapd/overlord/state"
+
+ . "gopkg.in/check.v1"
+)
+
+type progressAdapterTestSuite struct{}
+
+var _ = Suite(&progressAdapterTestSuite{})
+
+func (s *progressAdapterTestSuite) TestProgressAdapterWrite(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ p := TaskProgressAdapter{
+ task: st.NewTask("op", "msg"),
+ }
+ st.Unlock()
+
+ p.Start("msg", 161803)
+ c.Check(p.total, Equals, float64(161803))
+
+ p.Write([]byte("some-bytes"))
+ c.Check(p.current, Equals, float64(len("some-bytes")))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package snapstate implements the manager and state aspects responsible for the installation and removal of snaps.
+package snapstate
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+// SnapManager is responsible for the installation and removal of snaps.
+type SnapManager struct {
+ state *state.State
+ backend managerBackend
+
+ runner *state.TaskRunner
+}
+
+// SnapSetup holds the necessary snap details to perform most snap manager tasks.
+type SnapSetup struct {
+ // FIXME: rename to RequestedChannel to convey the meaning better
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+
+ Flags
+
+ SnapPath string `json:"snap-path,omitempty"`
+
+ DownloadInfo *snap.DownloadInfo `json:"download-info,omitempty"`
+ SideInfo *snap.SideInfo `json:"side-info,omitempty"`
+}
+
+func (snapsup *SnapSetup) Name() string {
+ if snapsup.SideInfo.RealName == "" {
+ panic("SnapSetup.SideInfo.RealName not set")
+ }
+ return snapsup.SideInfo.RealName
+}
+
+func (snapsup *SnapSetup) Revision() snap.Revision {
+ return snapsup.SideInfo.Revision
+}
+
+func (snapsup *SnapSetup) placeInfo() snap.PlaceInfo {
+ return snap.MinimalPlaceInfo(snapsup.Name(), snapsup.Revision())
+}
+
+func (snapsup *SnapSetup) MountDir() string {
+ return snap.MountDir(snapsup.Name(), snapsup.Revision())
+}
+
+func (snapsup *SnapSetup) MountFile() string {
+ return snap.MountFile(snapsup.Name(), snapsup.Revision())
+}
+
+// SnapState holds the state for a snap installed in the system.
+type SnapState struct {
+ SnapType string `json:"type"` // Use Type and SetType
+ Sequence []*snap.SideInfo `json:"sequence"`
+ Active bool `json:"active,omitempty"`
+ // Current indicates the current active revision if Active is
+ // true or the last active revision if Active is false
+ // (usually while a snap is being operated on or disabled)
+ Current snap.Revision `json:"current"`
+ Channel string `json:"channel,omitempty"`
+ Flags
+}
+
+// Type returns the type of the snap or an error.
+// Should never error if Current is not nil.
+func (snapst *SnapState) Type() (snap.Type, error) {
+ if snapst.SnapType == "" {
+ return snap.Type(""), fmt.Errorf("snap type unset")
+ }
+ return snap.Type(snapst.SnapType), nil
+}
+
+// SetType records the type of the snap.
+func (snapst *SnapState) SetType(typ snap.Type) {
+ snapst.SnapType = string(typ)
+}
+
+// HasCurrent returns whether snapst.Current is set.
+func (snapst *SnapState) HasCurrent() bool {
+ if snapst.Current.Unset() {
+ if len(snapst.Sequence) > 0 {
+ panic(fmt.Sprintf("snapst.Current and snapst.Sequence out of sync: %#v %#v", snapst.Current, snapst.Sequence))
+ }
+
+ return false
+ }
+ return true
+}
+
+// LocalRevision returns the "latest" local revision. Local revisions
+// start at -1 and are counted down.
+func (snapst *SnapState) LocalRevision() snap.Revision {
+ var local snap.Revision
+ for _, si := range snapst.Sequence {
+ if si.Revision.Local() && si.Revision.N < local.N {
+ local = si.Revision
+ }
+ }
+ return local
+}
+
+// TODO: unexport CurrentSideInfo and HasCurrent?
+
+// CurrentSideInfo returns the side info for the revision indicated by snapst.Current in the snap revision sequence if there is one.
+func (snapst *SnapState) CurrentSideInfo() *snap.SideInfo {
+ if !snapst.HasCurrent() {
+ return nil
+ }
+ if idx := snapst.LastIndex(snapst.Current); idx >= 0 {
+ return snapst.Sequence[idx]
+ }
+ panic("cannot find snapst.Current in the snapst.Sequence")
+}
+
+func (snapst *SnapState) previousSideInfo() *snap.SideInfo {
+ n := len(snapst.Sequence)
+ if n < 2 {
+ return nil
+ }
+ // find "current" and return the one before that
+ currentIndex := snapst.LastIndex(snapst.Current)
+ if currentIndex <= 0 {
+ return nil
+ }
+ return snapst.Sequence[currentIndex-1]
+}
+
+// LastIndex returns the last index of the given revision in the
+// snapst.Sequence
+func (snapst *SnapState) LastIndex(revision snap.Revision) int {
+ for i := len(snapst.Sequence) - 1; i >= 0; i-- {
+ if snapst.Sequence[i].Revision == revision {
+ return i
+ }
+ }
+ return -1
+}
+
+// Block returns revisions that should be blocked on refreshes,
+// computed from Sequence[currentRevisionIndex+1:].
+func (snapst *SnapState) Block() []snap.Revision {
+ // return revisions from Sequence[currentIndex:]
+ currentIndex := snapst.LastIndex(snapst.Current)
+ if currentIndex < 0 || currentIndex+1 == len(snapst.Sequence) {
+ return nil
+ }
+ out := make([]snap.Revision, len(snapst.Sequence)-currentIndex-1)
+ for i, si := range snapst.Sequence[currentIndex+1:] {
+ out[i] = si.Revision
+ }
+ return out
+}
+
+var ErrNoCurrent = errors.New("snap has no current revision")
+
+// Retrieval functions
+var readInfo = readInfoAnyway
+
+func readInfoAnyway(name string, si *snap.SideInfo) (*snap.Info, error) {
+ info, err := snap.ReadInfo(name, si)
+ if _, ok := err.(*snap.NotFoundError); ok {
+ reason := fmt.Sprintf("cannot read snap %q: %s", name, err)
+ info := &snap.Info{
+ SuggestedName: name,
+ Broken: reason,
+ }
+ info.Apps = snap.GuessAppsForBroken(info)
+ if si != nil {
+ info.SideInfo = *si
+ }
+ return info, nil
+ }
+ return info, err
+}
+
+// CurrentInfo returns the information about the current active revision or the last active revision (if the snap is inactive). It returns the ErrNoCurrent error if snapst.Current is unset.
+func (snapst *SnapState) CurrentInfo() (*snap.Info, error) {
+ cur := snapst.CurrentSideInfo()
+ if cur == nil {
+ return nil, ErrNoCurrent
+ }
+ return readInfo(cur.RealName, cur)
+}
+
+func revisionInSequence(snapst *SnapState, needle snap.Revision) bool {
+ for _, si := range snapst.Sequence {
+ if si.Revision == needle {
+ return true
+ }
+ }
+ return false
+}
+
+func userFromUserID(st *state.State, userID int) (*auth.UserState, error) {
+ if userID == 0 {
+ return nil, nil
+ }
+ return auth.User(st, userID)
+}
+
+type cachedStoreKey struct{}
+
+// ReplaceStore replaces the store used by the manager.
+func ReplaceStore(state *state.State, store StoreService) {
+ state.Cache(cachedStoreKey{}, store)
+}
+
+func cachedStore(st *state.State) StoreService {
+ ubuntuStore := st.Cached(cachedStoreKey{})
+ if ubuntuStore == nil {
+ return nil
+ }
+ return ubuntuStore.(StoreService)
+}
+
+// the store implementation has the interface consumed here
+var _ StoreService = (*store.Store)(nil)
+
+// Store returns the store service used by the snapstate package.
+func Store(st *state.State) StoreService {
+ if cachedStore := cachedStore(st); cachedStore != nil {
+ return cachedStore
+ }
+ panic("internal error: needing the store before managers have initialized it")
+}
+
+func updateInfo(st *state.State, snapst *SnapState, channel string, userID int) (*snap.Info, error) {
+ user, err := userFromUserID(st, userID)
+ if err != nil {
+ return nil, err
+ }
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ if curInfo.SnapID == "" { // covers also trymode
+ return nil, fmt.Errorf("cannot refresh local snap %q", curInfo.Name())
+ }
+
+ refreshCand := &store.RefreshCandidate{
+ // the desired channel
+ Channel: channel,
+ SnapID: curInfo.SnapID,
+ Revision: curInfo.Revision,
+ Epoch: curInfo.Epoch,
+ }
+
+ theStore := Store(st)
+ st.Unlock() // calls to the store should be done without holding the state lock
+ res, err := theStore.ListRefresh([]*store.RefreshCandidate{refreshCand}, user)
+ st.Lock()
+ if err != nil {
+ return nil, fmt.Errorf("cannot get refresh information for snap %q: %s", curInfo.Name(), err)
+ }
+ if len(res) == 0 {
+ return nil, &snap.NoUpdateAvailableError{Snap: curInfo.Name()}
+ }
+
+ return res[0], nil
+}
+
+func snapInfo(st *state.State, name, channel string, revision snap.Revision, userID int) (*snap.Info, error) {
+ user, err := userFromUserID(st, userID)
+ if err != nil {
+ return nil, err
+ }
+ theStore := Store(st)
+ st.Unlock() // calls to the store should be done without holding the state lock
+ spec := store.SnapSpec{
+ Name: name,
+ Channel: channel,
+ Revision: revision,
+ }
+ snap, err := theStore.SnapInfo(spec, user)
+ st.Lock()
+ return snap, err
+}
+
+// Manager returns a new snap manager.
+func Manager(st *state.State) (*SnapManager, error) {
+ runner := state.NewTaskRunner(st)
+
+ m := &SnapManager{
+ state: st,
+ backend: backend.Backend{},
+ runner: runner,
+ }
+
+ // this handler does nothing
+ runner.AddHandler("nop", func(t *state.Task, _ *tomb.Tomb) error {
+ return nil
+ }, nil)
+
+ // install/update related
+ runner.AddHandler("prepare-snap", m.doPrepareSnap, m.undoPrepareSnap)
+ runner.AddHandler("download-snap", m.doDownloadSnap, m.undoPrepareSnap)
+ runner.AddHandler("mount-snap", m.doMountSnap, m.undoMountSnap)
+ runner.AddHandler("unlink-current-snap", m.doUnlinkCurrentSnap, m.undoUnlinkCurrentSnap)
+ runner.AddHandler("copy-snap-data", m.doCopySnapData, m.undoCopySnapData)
+ runner.AddCleanup("copy-snap-data", m.cleanupCopySnapData)
+ runner.AddHandler("link-snap", m.doLinkSnap, m.undoLinkSnap)
+ runner.AddHandler("start-snap-services", m.startSnapServices, m.stopSnapServices)
+
+ // FIXME: drop the task entirely after a while
+ // (having this wart here avoids yet-another-patch)
+ runner.AddHandler("cleanup", func(*state.Task, *tomb.Tomb) error { return nil }, nil)
+
+ // remove related
+ runner.AddHandler("stop-snap-services", m.stopSnapServices, m.startSnapServices)
+ runner.AddHandler("unlink-snap", m.doUnlinkSnap, nil)
+ runner.AddHandler("clear-snap", m.doClearSnapData, nil)
+ runner.AddHandler("discard-snap", m.doDiscardSnap, nil)
+
+ // alias related
+ runner.AddHandler("alias", m.doAlias, m.undoAlias)
+ runner.AddHandler("clear-aliases", m.doClearAliases, m.undoClearAliases)
+ runner.AddHandler("set-auto-aliases", m.doSetAutoAliases, m.undoClearAliases)
+ runner.AddHandler("setup-aliases", m.doSetupAliases, m.undoSetupAliases)
+ runner.AddHandler("remove-aliases", m.doRemoveAliases, m.doSetupAliases)
+
+ // control serialisation
+ runner.SetBlocked(m.blockedTask)
+
+ // test handlers
+ runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error {
+ return nil
+ }, nil)
+ runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error {
+ return fmt.Errorf("fake-install-snap-error errored")
+ }, nil)
+
+ return m, nil
+}
+
+func diskAliasTask(t *state.Task) bool {
+ kind := t.Kind()
+ return kind == "setup-aliases" || kind == "remove-aliases" || kind == "alias"
+}
+
+func (m *SnapManager) blockedTask(cand *state.Task, running []*state.Task) bool {
+ // aliases are global, serialize tasks operating on them
+ if diskAliasTask(cand) {
+ for _, t := range running {
+ if diskAliasTask(t) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// Ensure implements StateManager.Ensure.
+func (m *SnapManager) Ensure() error {
+ m.runner.Ensure()
+ return nil
+}
+
+// Wait implements StateManager.Wait.
+func (m *SnapManager) Wait() {
+ m.runner.Wait()
+}
+
+// Stop implements StateManager.Stop.
+func (m *SnapManager) Stop() {
+ m.runner.Stop()
+}
+
+// TaskSnapSetup returns the SnapSetup with task params hold by or referred to by the the task.
+func TaskSnapSetup(t *state.Task) (*SnapSetup, error) {
+ var snapsup SnapSetup
+
+ err := t.Get("snap-setup", &snapsup)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if err == nil {
+ return &snapsup, nil
+ }
+
+ var id string
+ err = t.Get("snap-setup-task", &id)
+ if err != nil {
+ return nil, err
+ }
+
+ ts := t.State().Task(id)
+ if err := ts.Get("snap-setup", &snapsup); err != nil {
+ return nil, err
+ }
+ return &snapsup, nil
+}
+
+func snapSetupAndState(t *state.Task) (*SnapSetup, *SnapState, error) {
+ snapsup, err := TaskSnapSetup(t)
+ if err != nil {
+ return nil, nil, err
+ }
+ var snapst SnapState
+ err = Get(t.State(), snapsup.Name(), &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, nil, err
+ }
+ return snapsup, &snapst, nil
+}
+
+func (m *SnapManager) doPrepareSnap(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ st.Unlock()
+ if err != nil {
+ return err
+ }
+
+ if snapsup.Revision().Unset() {
+ // Local revisions start at -1 and go down.
+ revision := snapst.LocalRevision()
+ if revision.Unset() || revision.N > 0 {
+ revision = snap.R(-1)
+ } else {
+ revision.N--
+ }
+ if !revision.Local() {
+ panic("internal error: invalid local revision built: " + revision.String())
+ }
+ snapsup.SideInfo.Revision = revision
+ }
+
+ st.Lock()
+ t.Set("snap-setup", snapsup)
+ st.Unlock()
+ return nil
+}
+
+func (m *SnapManager) undoPrepareSnap(t *state.Task, _ *tomb.Tomb) error {
+ // FIXME: remove the entire function
+ return nil
+}
+
+func (m *SnapManager) doDownloadSnap(t *state.Task, tomb *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ snapsup, err := TaskSnapSetup(t)
+ st.Unlock()
+ if err != nil {
+ return err
+ }
+
+ meter := &TaskProgressAdapter{task: t}
+
+ st.Lock()
+ theStore := Store(st)
+ user, err := userFromUserID(st, snapsup.UserID)
+ st.Unlock()
+ if err != nil {
+ return err
+ }
+
+ targetFn := snapsup.MountFile()
+ if snapsup.DownloadInfo == nil {
+ var storeInfo *snap.Info
+ // COMPATIBILITY - this task was created from an older version
+ // of snapd that did not store the DownloadInfo in the state
+ // yet.
+ spec := store.SnapSpec{
+ Name: snapsup.Name(),
+ Channel: snapsup.Channel,
+ Revision: snapsup.Revision(),
+ }
+ storeInfo, err = theStore.SnapInfo(spec, user)
+ if err != nil {
+ return err
+ }
+ err = theStore.Download(tomb.Context(nil), snapsup.Name(), targetFn, &storeInfo.DownloadInfo, meter, user)
+ snapsup.SideInfo = &storeInfo.SideInfo
+ } else {
+ err = theStore.Download(tomb.Context(nil), snapsup.Name(), targetFn, snapsup.DownloadInfo, meter, user)
+ }
+ if err != nil {
+ return err
+ }
+
+ snapsup.SnapPath = targetFn
+
+ // update the snap setup for the follow up tasks
+ st.Lock()
+ t.Set("snap-setup", snapsup)
+ st.Unlock()
+
+ return nil
+}
+
+func (m *SnapManager) doUnlinkSnap(t *state.Task, _ *tomb.Tomb) error {
+ // invoked only if snap has a current active revision
+
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ info, err := Info(t.State(), snapsup.Name(), snapsup.Revision())
+ if err != nil {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ st.Unlock() // pb itself will ask for locking
+ err = m.backend.UnlinkSnap(info, pb)
+ st.Lock()
+ if err != nil {
+ return err
+ }
+
+ // mark as inactive
+ snapst.Active = false
+ Set(st, snapsup.Name(), snapst)
+ return nil
+}
+
+func (m *SnapManager) doClearSnapData(t *state.Task, _ *tomb.Tomb) error {
+ t.State().Lock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ t.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ t.State().Lock()
+ info, err := Info(t.State(), snapsup.Name(), snapsup.Revision())
+ t.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ if err = m.backend.RemoveSnapData(info); err != nil {
+ return err
+ }
+
+ // Only remove data common between versions if this is the last version
+ if len(snapst.Sequence) == 1 {
+ if err = m.backend.RemoveSnapCommonData(info); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (m *SnapManager) doDiscardSnap(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ st.Unlock()
+ if err != nil {
+ return err
+ }
+
+ if snapst.Current == snapsup.Revision() && snapst.Active {
+ return fmt.Errorf("internal error: cannot discard snap %q: still active", snapsup.Name())
+ }
+
+ if len(snapst.Sequence) == 1 {
+ snapst.Sequence = nil
+ snapst.Current = snap.Revision{}
+ } else {
+ newSeq := make([]*snap.SideInfo, 0, len(snapst.Sequence))
+ for _, si := range snapst.Sequence {
+ if si.Revision == snapsup.Revision() {
+ // leave out
+ continue
+ }
+ newSeq = append(newSeq, si)
+ }
+ snapst.Sequence = newSeq
+ if snapst.Current == snapsup.Revision() {
+ snapst.Current = newSeq[len(newSeq)-1].Revision
+ }
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ typ, err := snapst.Type()
+ if err != nil {
+ return err
+ }
+ err = m.backend.RemoveSnapFiles(snapsup.placeInfo(), typ, pb)
+ if err != nil {
+ st.Lock()
+ t.Errorf("cannot remove snap file %q, will retry in 3 mins: %s", snapsup.Name(), err)
+ st.Unlock()
+ return &state.Retry{After: 3 * time.Minute}
+ }
+ if len(snapst.Sequence) == 0 {
+ err = m.backend.DiscardSnapNamespace(snapsup.Name())
+ if err != nil {
+ st.Lock()
+ t.Errorf("cannot discard snap namespace %q, will retry in 3 mins: %s", snapsup.Name(), err)
+ st.Unlock()
+ return &state.Retry{After: 3 * time.Minute}
+ }
+ }
+ st.Lock()
+ Set(st, snapsup.Name(), snapst)
+ st.Unlock()
+ return nil
+}
+
+func (m *SnapManager) undoMountSnap(t *state.Task, _ *tomb.Tomb) error {
+ t.State().Lock()
+ snapsup, err := TaskSnapSetup(t)
+ t.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ t.State().Lock()
+ var typ snap.Type
+ err = t.Get("snap-type", &typ)
+ t.State().Unlock()
+ // backward compatibility
+ if err == state.ErrNoState {
+ typ = "app"
+ } else if err != nil {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ return m.backend.UndoSetupSnap(snapsup.placeInfo(), typ, pb)
+}
+
+func (m *SnapManager) doMountSnap(t *state.Task, _ *tomb.Tomb) error {
+ t.State().Lock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ t.State().Unlock()
+ if err != nil {
+ return err
+ }
+ curInfo, err := snapst.CurrentInfo()
+ if err != nil && err != ErrNoCurrent {
+ return err
+ }
+
+ m.backend.CurrentInfo(curInfo)
+
+ if err := checkSnap(t.State(), snapsup.SnapPath, snapsup.SideInfo, curInfo, snapsup.Flags); err != nil {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ // TODO Use snapsup.Revision() to obtain the right info to mount
+ // instead of assuming the candidate is the right one.
+ if err := m.backend.SetupSnap(snapsup.SnapPath, snapsup.SideInfo, pb); err != nil {
+ return err
+ }
+
+ // set snapst type for undoMountSnap
+ newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo)
+ if err != nil {
+ return err
+ }
+ t.State().Lock()
+ t.Set("snap-type", newInfo.Type)
+ t.State().Unlock()
+
+ if snapsup.Flags.RemoveSnapPath {
+ if err := os.Remove(snapsup.SnapPath); err != nil {
+ logger.Noticef("Failed to cleanup %s: %s", snapsup.SnapPath, err)
+ }
+ }
+
+ return nil
+}
+
+func (m *SnapManager) undoUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ oldInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+
+ snapst.Active = true
+ st.Unlock()
+ err = m.backend.LinkSnap(oldInfo)
+ st.Lock()
+ if err != nil {
+ return err
+ }
+
+ // mark as active again
+ Set(st, snapsup.Name(), snapst)
+
+ return nil
+
+}
+
+func (m *SnapManager) doUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ oldInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+
+ snapst.Active = false
+
+ pb := &TaskProgressAdapter{task: t}
+ st.Unlock() // pb itself will ask for locking
+ err = m.backend.UnlinkSnap(oldInfo, pb)
+ st.Lock()
+ if err != nil {
+ return err
+ }
+
+ // mark as inactive
+ Set(st, snapsup.Name(), snapst)
+ return nil
+}
+
+func (m *SnapManager) undoCopySnapData(t *state.Task, _ *tomb.Tomb) error {
+ t.State().Lock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ t.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo)
+ if err != nil {
+ return err
+ }
+
+ oldInfo, err := snapst.CurrentInfo()
+ if err != nil && err != ErrNoCurrent {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ return m.backend.UndoCopySnapData(newInfo, oldInfo, pb)
+}
+
+func (m *SnapManager) doCopySnapData(t *state.Task, _ *tomb.Tomb) error {
+ t.State().Lock()
+ snapsup, snapst, err := snapSetupAndState(t)
+ t.State().Unlock()
+ if err != nil {
+ return err
+ }
+
+ newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo)
+ if err != nil {
+ return err
+ }
+
+ oldInfo, err := snapst.CurrentInfo()
+ if err != nil && err != ErrNoCurrent {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ return m.backend.CopySnapData(newInfo, oldInfo, pb)
+}
+
+func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ cand := snapsup.SideInfo
+ m.backend.Candidate(cand)
+
+ oldCandidateIndex := snapst.LastIndex(cand.Revision)
+
+ if oldCandidateIndex < 0 {
+ snapst.Sequence = append(snapst.Sequence, cand)
+ } else if !snapsup.Revert {
+ // remove the old candidate from the sequence, add it at the end
+ copy(snapst.Sequence[oldCandidateIndex:len(snapst.Sequence)-1], snapst.Sequence[oldCandidateIndex+1:])
+ snapst.Sequence[len(snapst.Sequence)-1] = cand
+ }
+
+ oldCurrent := snapst.Current
+ snapst.Current = cand.Revision
+ snapst.Active = true
+ oldChannel := snapst.Channel
+ if snapsup.Channel != "" {
+ snapst.Channel = snapsup.Channel
+ }
+ oldTryMode := snapst.TryMode
+ snapst.TryMode = snapsup.TryMode
+ oldDevMode := snapst.DevMode
+ snapst.DevMode = snapsup.DevMode
+ oldJailMode := snapst.JailMode
+ snapst.JailMode = snapsup.JailMode
+ oldClassic := snapst.Classic
+ snapst.Classic = snapsup.Classic
+
+ newInfo, err := readInfo(snapsup.Name(), cand)
+ if err != nil {
+ return err
+ }
+
+ // record type
+ snapst.SetType(newInfo.Type)
+
+ st.Unlock()
+ // XXX: this block is slightly ugly, find a pattern when we have more examples
+ err = m.backend.LinkSnap(newInfo)
+ if err != nil {
+ pb := &TaskProgressAdapter{task: t}
+ err := m.backend.UnlinkSnap(newInfo, pb)
+ if err != nil {
+ st.Lock()
+ t.Errorf("cannot cleanup failed attempt at making snap %q available to the system: %v", snapsup.Name(), err)
+ st.Unlock()
+ }
+ }
+ st.Lock()
+ if err != nil {
+ return err
+ }
+
+ // save for undoLinkSnap
+ t.Set("old-trymode", oldTryMode)
+ t.Set("old-devmode", oldDevMode)
+ t.Set("old-jailmode", oldJailMode)
+ t.Set("old-classic", oldClassic)
+ t.Set("old-channel", oldChannel)
+ t.Set("old-current", oldCurrent)
+ t.Set("old-candidate-index", oldCandidateIndex)
+ // Do at the end so we only preserve the new state if it worked.
+ Set(st, snapsup.Name(), snapst)
+ // Make sure if state commits and snapst is mutated we won't be rerun
+ t.SetStatus(state.DoneStatus)
+
+ // if we just installed a core snap, request a restart
+ // so that we switch executing its snapd
+ if release.OnClassic && newInfo.Type == snap.TypeOS {
+ t.Logf("Requested daemon restart.")
+ st.Unlock()
+ st.RequestRestart(state.RestartDaemon)
+ st.Lock()
+ }
+ if !release.OnClassic && boot.KernelOrOsRebootRequired(newInfo) {
+ t.Logf("Requested system restart.")
+ st.Unlock()
+ st.RequestRestart(state.RestartSystem)
+ st.Lock()
+ }
+
+ return nil
+}
+
+func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ snapsup, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ var oldChannel string
+ err = t.Get("old-channel", &oldChannel)
+ if err != nil {
+ return err
+ }
+ var oldTryMode bool
+ err = t.Get("old-trymode", &oldTryMode)
+ if err != nil {
+ return err
+ }
+ var oldDevMode bool
+ err = t.Get("old-devmode", &oldDevMode)
+ if err != nil {
+ return err
+ }
+ var oldJailMode bool
+ err = t.Get("old-jailmode", &oldJailMode)
+ if err != nil {
+ return err
+ }
+ var oldClassic bool
+ err = t.Get("old-classic", &oldClassic)
+ if err != nil {
+ return err
+ }
+ var oldCurrent snap.Revision
+ err = t.Get("old-current", &oldCurrent)
+ if err != nil {
+ return err
+ }
+ var oldCandidateIndex int
+ if err := t.Get("old-candidate-index", &oldCandidateIndex); err != nil {
+ return err
+ }
+
+ isRevert := snapsup.Revert
+
+ // relinking of the old snap is done in the undo of unlink-current-snap
+ currentIndex := snapst.LastIndex(snapst.Current)
+ if currentIndex < 0 {
+ return fmt.Errorf("internal error: cannot find revision %d in %v for undoing the added revision", snapsup.SideInfo.Revision, snapst.Sequence)
+ }
+
+ if oldCandidateIndex < 0 {
+ snapst.Sequence = append(snapst.Sequence[:currentIndex], snapst.Sequence[currentIndex+1:]...)
+ } else if !isRevert {
+ oldCand := snapst.Sequence[currentIndex]
+ copy(snapst.Sequence[oldCandidateIndex+1:], snapst.Sequence[oldCandidateIndex:])
+ snapst.Sequence[oldCandidateIndex] = oldCand
+ }
+ snapst.Current = oldCurrent
+ snapst.Active = false
+ snapst.Channel = oldChannel
+ snapst.TryMode = oldTryMode
+ snapst.DevMode = oldDevMode
+ snapst.JailMode = oldJailMode
+ snapst.Classic = oldClassic
+
+ newInfo, err := readInfo(snapsup.Name(), snapsup.SideInfo)
+ if err != nil {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ st.Unlock() // pb itself will ask for locking
+ err = m.backend.UnlinkSnap(newInfo, pb)
+ st.Lock()
+ if err != nil {
+ return err
+ }
+
+ // mark as inactive
+ Set(st, snapsup.Name(), snapst)
+ // Make sure if state commits and snapst is mutated we won't be rerun
+ t.SetStatus(state.UndoneStatus)
+ return nil
+}
+
+func (m *SnapManager) startSnapServices(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ _, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ currentInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ st.Unlock()
+ err = m.backend.StartSnapServices(currentInfo, pb)
+ st.Lock()
+ return err
+}
+
+func (m *SnapManager) stopSnapServices(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ _, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ currentInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+
+ pb := &TaskProgressAdapter{task: t}
+ st.Unlock()
+ err = m.backend.StopSnapServices(currentInfo, pb)
+ st.Lock()
+
+ return err
+}
+
+func (m *SnapManager) cleanupCopySnapData(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ if t.Status() != state.DoneStatus {
+ // it failed
+ return nil
+ }
+
+ _, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ return err
+ }
+
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return err
+ }
+
+ m.backend.ClearTrashedData(info)
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapstate_test
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/snapstate/backend"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/store"
+
+ // So it registers Configure.
+ _ "github.com/snapcore/snapd/overlord/configstate"
+)
+
+func TestSnapManager(t *testing.T) { TestingT(t) }
+
+type snapmgrTestSuite struct {
+ state *state.State
+ snapmgr *snapstate.SnapManager
+
+ fakeBackend *fakeSnappyBackend
+ fakeStore *fakeStore
+
+ user *auth.UserState
+
+ reset func()
+}
+
+func (s *snapmgrTestSuite) settle() {
+ // FIXME: use the real settle here
+ for i := 0; i < 50; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+}
+
+var _ = Suite(&snapmgrTestSuite{})
+
+func (s *snapmgrTestSuite) SetUpTest(c *C) {
+ s.fakeBackend = &fakeSnappyBackend{}
+ s.state = state.New(nil)
+ s.fakeStore = &fakeStore{
+ fakeCurrentProgress: 75,
+ fakeTotalProgress: 100,
+ fakeBackend: s.fakeBackend,
+ state: s.state,
+ }
+
+ var err error
+ s.snapmgr, err = snapstate.Manager(s.state)
+ c.Assert(err, IsNil)
+ s.snapmgr.AddForeignTaskHandlers(s.fakeBackend)
+
+ snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend)
+
+ restore1 := snapstate.MockReadInfo(s.fakeBackend.ReadInfo)
+ restore2 := snapstate.MockOpenSnapFile(s.fakeBackend.OpenSnapFile)
+
+ s.reset = func() {
+ restore2()
+ restore1()
+ }
+
+ s.state.Lock()
+ snapstate.ReplaceStore(s.state, s.fakeStore)
+ s.user, err = auth.NewUser(s.state, "username", "email@test.com", "macaroon", []string{"discharge"})
+ c.Assert(err, IsNil)
+ s.state.Unlock()
+
+ snapstate.AutoAliases = func(*state.State, *snap.Info) ([]string, error) {
+ return nil, nil
+ }
+}
+
+func (s *snapmgrTestSuite) TearDownTest(c *C) {
+ snapstate.ValidateRefreshes = nil
+ snapstate.AutoAliases = nil
+ s.reset()
+}
+
+func (s *snapmgrTestSuite) TestStore(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ sto := &store.Store{}
+ snapstate.ReplaceStore(s.state, sto)
+ store1 := snapstate.Store(s.state)
+ c.Check(store1, Equals, sto)
+
+ // cached
+ store2 := snapstate.Store(s.state)
+ c.Check(store2, Equals, sto)
+}
+
+const (
+ unlinkBefore = 1 << iota
+ cleanupAfter
+)
+
+func taskKinds(tasks []*state.Task) []string {
+ kinds := make([]string, len(tasks))
+ for i, task := range tasks {
+ kinds[i] = task.Kind()
+ }
+ return kinds
+}
+
+func verifyInstallUpdateTasks(c *C, opts, discards int, ts *state.TaskSet, st *state.State) {
+ kinds := taskKinds(ts.Tasks())
+
+ expected := []string{
+ "download-snap",
+ "validate-snap",
+ "mount-snap",
+ }
+ if opts&unlinkBefore != 0 {
+ expected = append(expected,
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-current-snap",
+ )
+ }
+ expected = append(expected,
+ "copy-snap-data",
+ "setup-profiles",
+ "link-snap",
+ "set-auto-aliases",
+ "setup-aliases",
+ "start-snap-services",
+ )
+ for i := 0; i < discards; i++ {
+ expected = append(expected,
+ "clear-snap",
+ "discard-snap",
+ )
+ }
+ if opts&cleanupAfter != 0 {
+ expected = append(expected,
+ "cleanup",
+ )
+ }
+ expected = append(expected,
+ "run-hook",
+ )
+
+ c.Assert(kinds, DeepEquals, expected)
+}
+
+func (s *snapmgrTestSuite) TestLastIndexFindsLast(c *C) {
+ snapst := &snapstate.SnapState{Sequence: []*snap.SideInfo{
+ {Revision: snap.R(7)},
+ {Revision: snap.R(11)},
+ {Revision: snap.R(11)},
+ }}
+ c.Check(snapst.LastIndex(snap.R(11)), Equals, 2)
+}
+
+func (s *snapmgrTestSuite) TestInstallDevModeConfinementFiltering(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // if a snap is devmode, you can't install it without --devmode
+ _, err := snapstate.Install(s.state, "some-snap", "channel-for-devmode", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap not found`)
+
+ // if a snap is devmode, you *can* install it with --devmode
+ _, err = snapstate.Install(s.state, "some-snap", "channel-for-devmode", snap.R(0), s.user.ID, snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+
+ // if a snap is *not* devmode, you can still install it with --devmode
+ _, err = snapstate.Install(s.state, "some-snap", "channel-for-strict", snap.R(0), s.user.ID, snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestInstallClassicConfinementFiltering(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // if a snap is classic, you can't install it without --classic
+ _, err := snapstate.Install(s.state, "some-snap", "channel-for-classic", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap not found`)
+
+ // if a snap is classic, you *can* install it with --classic
+ _, err = snapstate.Install(s.state, "some-snap", "channel-for-classic", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
+ c.Assert(err, IsNil)
+
+ // if a snap is *not* classic, you can still install it with --classic
+ _, err = snapstate.Install(s.state, "some-snap", "channel-for-strict", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
+ c.Assert(err, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestInstallTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ verifyInstallUpdateTasks(c, 0, 0, ts, s.state)
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+}
+
+func (s *snapmgrTestSuite) TestRevertTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(7)},
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "prepare-snap",
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-current-snap",
+ "setup-profiles",
+ "link-snap",
+ "set-auto-aliases",
+ "setup-aliases",
+ "start-snap-services",
+ "run-hook",
+ })
+}
+
+func (s *snapmgrTestSuite) TestUpdateCreatesGCTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
+ },
+ Current: snap.R(4),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ verifyInstallUpdateTasks(c, unlinkBefore|cleanupAfter, 2, ts, s.state)
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+}
+
+func (s *snapmgrTestSuite) TestUpdateCreatesDiscardAfterCurrentTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ verifyInstallUpdateTasks(c, unlinkBefore|cleanupAfter, 3, ts, s.state)
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+}
+
+func (s *snapmgrTestSuite) TestUpdateMany(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ updates, tts, err := snapstate.UpdateMany(s.state, nil, 0)
+ c.Assert(err, IsNil)
+ c.Assert(tts, HasLen, 1)
+ c.Check(updates, DeepEquals, []string{"some-snap"})
+
+ ts := tts[0]
+ verifyInstallUpdateTasks(c, unlinkBefore|cleanupAfter, 3, ts, s.state)
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyDevModeConfinementFiltering(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel-for-devmode",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ // updated snap is devmode, updatemany doesn't update it
+ _, tts, _ := snapstate.UpdateMany(s.state, []string{"some-snap"}, s.user.ID)
+ // FIXME: UpdateMany will not error out in this case (daemon catches this case, with a weird error)
+ c.Assert(tts, HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyClassicConfinementFiltering(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel-for-classic",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ // if a snap installed without --classic gets a classic update it isn't installed
+ _, tts, _ := snapstate.UpdateMany(s.state, []string{"some-snap"}, s.user.ID)
+ // FIXME: UpdateMany will not error out in this case (daemon catches this case, with a weird error)
+ c.Assert(tts, HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyClassic(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel-for-classic",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ Flags: snapstate.Flags{Classic: true},
+ })
+
+ // snap installed with classic: refresh gets classic
+ _, tts, err := snapstate.UpdateMany(s.state, []string{"some-snap"}, s.user.ID)
+ c.Assert(err, IsNil)
+ c.Assert(tts, HasLen, 1)
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyDevMode(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Flags: snapstate.Flags{DevMode: true},
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ updates, _, err := snapstate.UpdateMany(s.state, []string{"some-snap"}, 0)
+ c.Assert(err, IsNil)
+ c.Check(updates, HasLen, 1)
+}
+
+func (s *snapmgrTestSuite) TestUpdateAllDevMode(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Flags: snapstate.Flags{DevMode: true},
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ updates, _, err := snapstate.UpdateMany(s.state, nil, 0)
+ c.Assert(err, IsNil)
+ c.Check(updates, HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyValidateRefreshes(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ validateCalled := false
+ validateRefreshes := func(st *state.State, refreshes []*snap.Info, userID int) ([]*snap.Info, error) {
+ validateCalled = true
+ c.Check(refreshes, HasLen, 1)
+ c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
+ c.Check(refreshes[0].Revision, Equals, snap.R(11))
+ return refreshes, nil
+ }
+ // hook it up
+ snapstate.ValidateRefreshes = validateRefreshes
+
+ updates, tts, err := snapstate.UpdateMany(s.state, nil, 0)
+ c.Assert(err, IsNil)
+ c.Assert(tts, HasLen, 1)
+ c.Check(updates, DeepEquals, []string{"some-snap"})
+ verifyInstallUpdateTasks(c, unlinkBefore|cleanupAfter, 0, tts[0], s.state)
+
+ c.Check(validateCalled, Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyValidateRefreshesUnhappy(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ })
+
+ validateErr := errors.New("refresh control error")
+ validateRefreshes := func(st *state.State, refreshes []*snap.Info, userID int) ([]*snap.Info, error) {
+ c.Check(refreshes, HasLen, 1)
+ c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
+ c.Check(refreshes[0].Revision, Equals, snap.R(11))
+ return nil, validateErr
+ }
+ // hook it up
+ snapstate.ValidateRefreshes = validateRefreshes
+
+ // refresh all => no error
+ updates, tts, err := snapstate.UpdateMany(s.state, nil, 0)
+ c.Assert(err, IsNil)
+ c.Check(tts, HasLen, 0)
+ c.Check(updates, HasLen, 0)
+
+ // refresh some-snap => report error
+ updates, tts, err = snapstate.UpdateMany(s.state, []string{"some-snap"}, 0)
+ c.Assert(err, Equals, validateErr)
+ c.Check(tts, HasLen, 0)
+ c.Check(updates, HasLen, 0)
+
+}
+
+func (s *snapmgrTestSuite) TestRevertCreatesNoGCTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(1)},
+ {RealName: "some-snap", Revision: snap.R(2)},
+ {RealName: "some-snap", Revision: snap.R(3)},
+ {RealName: "some-snap", Revision: snap.R(4)},
+ },
+ Current: snap.R(2),
+ })
+
+ ts, err := snapstate.RevertToRevision(s.state, "some-snap", snap.R(4), snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ // ensure that we do not run any form of garbage-collection
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "prepare-snap",
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-current-snap",
+ "setup-profiles",
+ "link-snap",
+ "set-auto-aliases",
+ "setup-aliases",
+ "start-snap-services",
+ "run-hook",
+ })
+}
+
+func (s *snapmgrTestSuite) TestEnableTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: false,
+ })
+
+ ts, err := snapstate.Enable(s.state, "some-snap")
+ c.Assert(err, IsNil)
+
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "prepare-snap",
+ "link-snap",
+ "setup-aliases",
+ "start-snap-services",
+ })
+}
+
+func (s *snapmgrTestSuite) TestDisableTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ ts, err := snapstate.Disable(s.state, "some-snap")
+ c.Assert(err, IsNil)
+
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-snap",
+ })
+}
+
+func (s *snapmgrTestSuite) TestEnableConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: false,
+ })
+
+ ts, err := snapstate.Enable(s.state, "some-snap")
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("enable", "...").AddAll(ts)
+
+ _, err = snapstate.Enable(s.state, "some-snap")
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestDisableConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ Active: true,
+ })
+
+ ts, err := snapstate.Disable(s.state, "some-snap")
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("install", "...").AddAll(ts)
+
+ _, err = snapstate.Disable(s.state, "some-snap")
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestDoInstallChannelDefault(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := snapstate.Install(s.state, "some-snap", "", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ var snapsup snapstate.SnapSetup
+ err = ts.Tasks()[0].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+
+ c.Check(snapsup.Channel, Equals, "stable")
+}
+
+func (s *snapmgrTestSuite) TestInstallRevision(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := snapstate.Install(s.state, "some-snap", "", snap.R(7), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ var snapsup snapstate.SnapSetup
+ err = ts.Tasks()[0].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+
+ c.Check(snapsup.Revision(), Equals, snap.R(7))
+}
+
+func (s *snapmgrTestSuite) TestInstallConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("install", "...").AddAll(ts)
+
+ _, err = snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestInstallAliasConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.state.Set("aliases", map[string]map[string]string{
+ "otherfoosnap": {
+ "foo.bar": "enabled",
+ },
+ })
+
+ _, err := snapstate.Install(s.state, "foo", "some-channel", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "foo" command namespace conflicts with enabled alias "foo\.bar" for "otherfoosnap"`)
+}
+
+// A sneakyStore changes the state when called
+type sneakyStore struct {
+ *fakeStore
+ state *state.State
+}
+
+func (s sneakyStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
+ s.state.Lock()
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}},
+ Current: snap.R(1),
+ })
+ s.state.Unlock()
+ return s.fakeStore.SnapInfo(spec, user)
+}
+
+func (s *snapmgrTestSuite) TestInstallStateConflict(c *C) {
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.ReplaceStore(s.state, sneakyStore{fakeStore: s.fakeStore, state: s.state})
+
+ _, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" state changed during install preparations`)
+}
+
+func (s *snapmgrTestSuite) TestInstallPathConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("install", "...").AddAll(ts)
+
+ mockSnap := makeTestSnap(c, "name: some-snap\nversion: 1.0")
+ _, err = snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "some-snap"}, mockSnap, "", snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestInstallPathMissingName(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ mockSnap := makeTestSnap(c, "name: some-snap\nversion: 1.0")
+ _, err := snapstate.InstallPath(s.state, &snap.SideInfo{}, mockSnap, "", snapstate.Flags{})
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`internal error: snap name to install %q not provided`, mockSnap))
+}
+
+func (s *snapmgrTestSuite) TestInstallPathSnapIDRevisionUnset(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ mockSnap := makeTestSnap(c, "name: some-snap\nversion: 1.0")
+ _, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "some-snap", SnapID: "snapididid"}, mockSnap, "", snapstate.Flags{})
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`internal error: snap id set to install %q but revision is unset`, mockSnap))
+}
+
+func (s *snapmgrTestSuite) TestUpdateTasksPropagatesErrors(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "fakestore-please-error-on-refresh", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ })
+
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `cannot get refresh information for snap "some-snap": failing as requested`)
+}
+
+func (s *snapmgrTestSuite) TestUpdateTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ validateCalled := false
+ happyValidateRefreshes := func(st *state.State, refreshes []*snap.Info, userID int) ([]*snap.Info, error) {
+ validateCalled = true
+ return refreshes, nil
+ }
+ // hook it up
+ snapstate.ValidateRefreshes = happyValidateRefreshes
+
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ verifyInstallUpdateTasks(c, unlinkBefore|cleanupAfter, 0, ts, s.state)
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+
+ c.Check(validateCalled, Equals, true)
+
+ var snapsup snapstate.SnapSetup
+ err = ts.Tasks()[0].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+
+ c.Check(snapsup.Channel, Equals, "some-channel")
+}
+
+func (s *snapmgrTestSuite) TestUpdateDevModeConfinementFiltering(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel-for-devmode",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ // updated snap is devmode, refresh without --devmode, do nothing
+ // TODO: better error message here
+ _, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has no updates available`)
+
+ // updated snap is devmode, refresh with --devmode
+ _, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestUpdateClassicConfinementFiltering(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel-for-classic",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ // updated snap is classic, refresh without --classic, do nothing
+ // TODO: better error message here
+ _, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has no updates available`)
+
+ // updated snap is classic, refresh with --classic
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
+ c.Assert(err, IsNil)
+
+ chg := s.state.NewChange("refresh", "refresh snap")
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // verify snap is in classic
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Classic, Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestUpdateClassicFromClassic(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel-for-classic",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ Flags: snapstate.Flags{Classic: true},
+ })
+
+ // snap installed with --classic, update needs classic, refresh with --classic works
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), Not(HasLen), 0)
+ snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0])
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Classic, Equals, true)
+
+ // devmode overrides the snapsetup classic flag
+ ts, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{DevMode: true})
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), Not(HasLen), 0)
+ snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Classic, Equals, false)
+
+ // jailmode overrides it too (you need to provide both)
+ ts, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{JailMode: true})
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), Not(HasLen), 0)
+ snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Classic, Equals, false)
+
+ // jailmode and classic together gets you both
+ ts, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{JailMode: true, Classic: true})
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), Not(HasLen), 0)
+ snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Classic, Equals, true)
+
+ // snap installed with --classic, update needs classic, refresh without --classic works
+ ts, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), Not(HasLen), 0)
+ snapsup, err = snapstate.TaskSnapSetup(ts.Tasks()[0])
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags.Classic, Equals, true)
+
+ chg := s.state.NewChange("refresh", "refresh snap")
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // verify snap is in classic
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Classic, Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestUpdateStrictFromClassic(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "channel",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ Flags: snapstate.Flags{Classic: true},
+ })
+
+ // snap installed with --classic, update does not need classic, refresh works without --classic
+ _, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ // snap installed with --classic, update does not need classic, refresh works with --classic
+ _, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
+ c.Assert(err, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestUpdateChannelFallback(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ var snapsup snapstate.SnapSetup
+ err = ts.Tasks()[0].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+
+ c.Check(snapsup.Channel, Equals, "edge")
+}
+
+func (s *snapmgrTestSuite) TestUpdateConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
+ Current: snap.R(7),
+ SnapType: "app",
+ })
+
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("refresh", "...").AddAll(ts)
+
+ _, err = snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestRemoveTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", Revision: snap.R(11)},
+ },
+ Current: snap.R(11),
+ })
+
+ ts, err := snapstate.Remove(s.state, "foo", snap.R(0))
+ c.Assert(err, IsNil)
+
+ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-snap",
+ "remove-profiles",
+ "clear-snap",
+ "discard-snap",
+ "clear-aliases",
+ "discard-conns",
+ })
+}
+
+func (s *snapmgrTestSuite) TestRemoveConflict(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}},
+ Current: snap.R(11),
+ })
+
+ ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0))
+ c.Assert(err, IsNil)
+ // need a change to make the tasks visible
+ s.state.NewChange("remove", "...").AddAll(ts)
+
+ _, err = snapstate.Remove(s.state, "some-snap", snap.R(0))
+ c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
+}
+
+func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "install a snap")
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // ensure all our tasks ran
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.IsReady(), Equals, true)
+ c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
+ macaroon: s.user.StoreMacaroon,
+ name: "some-snap",
+ }})
+ expected := fakeOps{
+ {
+ op: "storesvc-snap",
+ name: "some-snap",
+ revno: snap.R(42),
+ },
+ {
+ op: "storesvc-download",
+ name: "some-snap",
+ },
+ {
+ op: "validate-snap:Doing",
+ name: "some-snap",
+ revno: snap.R(42),
+ },
+ {
+ op: "current",
+ old: "<no-current>",
+ },
+ {
+ op: "open-snap-file",
+ name: "/var/lib/snapd/snaps/some-snap_42.snap",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "some-channel",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Revision: snap.R(42),
+ },
+ },
+ {
+ op: "setup-snap",
+ name: "/var/lib/snapd/snaps/some-snap_42.snap",
+ revno: snap.R(42),
+ },
+ {
+ op: "copy-data",
+ name: "/snap/some-snap/42",
+ old: "<no-old>",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(42),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "some-channel",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Revision: snap.R(42),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/42",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/42",
+ },
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(42),
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // check progress
+ ta := ts.Tasks()
+ task := ta[0]
+ _, cur, total := task.Progress()
+ c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
+ c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)
+ c.Check(task.Summary(), Equals, `Download snap "some-snap" (42) from channel "some-channel"`)
+
+ // check link/start snap summary
+ linkTask := ta[len(ta)-5]
+ c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (42) available to the system`)
+ startTask := ta[len(ta)-2]
+ c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (42) services`)
+
+ // verify snap-setup in the task state
+ var snapsup snapstate.SnapSetup
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
+ Channel: "some-channel",
+ UserID: s.user.ID,
+ SnapPath: "/var/lib/snapd/snaps/some-snap_42.snap",
+ DownloadInfo: &snap.DownloadInfo{
+ DownloadURL: "https://some-server.com/some/path.snap",
+ },
+ SideInfo: snapsup.SideInfo,
+ })
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(42),
+ Channel: "some-channel",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ })
+
+ // verify snaps in the system state
+ var snaps map[string]*snapstate.SnapState
+ err = s.state.Get("snaps", &snaps)
+ c.Assert(err, IsNil)
+
+ snapst := snaps["some-snap"]
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Channel, Equals, "some-channel")
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "some-channel",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Revision: snap.R(42),
+ })
+}
+
+func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ SnapID: "some-snap-id",
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("install", "install a snap")
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "storesvc-list-refresh",
+ cand: store.RefreshCandidate{
+ Channel: "some-channel",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ Epoch: "",
+ },
+ revno: snap.R(11),
+ },
+ {
+ op: "storesvc-download",
+ name: "some-snap",
+ },
+ {
+ op: "validate-snap:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "current",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "open-snap-file",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "setup-snap",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "copy-data",
+ name: "/snap/some-snap/11",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ }
+
+ // ensure all our tasks ran
+ c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
+ macaroon: s.user.StoreMacaroon,
+ name: "some-snap",
+ }})
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // check progress
+ task := ts.Tasks()[0]
+ _, cur, total := task.Progress()
+ c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
+ c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)
+
+ // verify snapSetup info
+ var snapsup snapstate.SnapSetup
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
+ Channel: "some-channel",
+ UserID: s.user.ID,
+
+ SnapPath: "/var/lib/snapd/snaps/some-snap_11.snap",
+ DownloadInfo: &snap.DownloadInfo{
+ DownloadURL: "https://some-server.com/some/path.snap",
+ },
+ SideInfo: snapsup.SideInfo,
+ })
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(11),
+ Channel: "some-channel",
+ SnapID: "some-snap-id",
+ })
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence, HasLen, 2)
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "",
+ Revision: snap.R(7),
+ })
+ c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "some-channel",
+ SnapID: "some-snap-id",
+ Revision: snap.R(11),
+ })
+}
+
+func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("install", "install a snap")
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.fakeBackend.linkSnapFailTrigger = "/snap/some-snap/11"
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "storesvc-list-refresh",
+ cand: store.RefreshCandidate{
+ Channel: "some-channel",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ Epoch: "",
+ },
+ revno: snap.R(11),
+ },
+ {
+ op: "storesvc-download",
+ name: "some-snap",
+ },
+ {
+ op: "validate-snap:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "current",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "open-snap-file",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "setup-snap",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "copy-data",
+ name: "/snap/some-snap/11",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "link-snap.failed",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "setup-profiles:Undoing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "undo-copy-snap-data",
+ name: "/snap/some-snap/11",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "undo-setup-snap",
+ name: "/snap/some-snap/11",
+ stype: "app",
+ },
+ }
+
+ // ensure all our tasks ran
+ c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
+ macaroon: s.user.StoreMacaroon,
+ name: "some-snap",
+ }})
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence, HasLen, 1)
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "",
+ Revision: snap.R(7),
+ })
+}
+
+func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Channel: "stable",
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("install", "install a snap")
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+ // sanity
+ c.Assert(last.Lanes(), HasLen, 1)
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ terr.JoinLane(last.Lanes()[0])
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "storesvc-list-refresh",
+ cand: store.RefreshCandidate{
+ Channel: "some-channel",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ Epoch: "",
+ },
+ revno: snap.R(11),
+ },
+ {
+ op: "storesvc-download",
+ name: "some-snap",
+ },
+ {
+ op: "validate-snap:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "current",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "open-snap-file",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "setup-snap",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "copy-data",
+ name: "/snap/some-snap/11",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "update-aliases",
+ },
+
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/11",
+ },
+ // undoing everything from here down...
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "matching-aliases",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "setup-profiles:Undoing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "undo-copy-snap-data",
+ name: "/snap/some-snap/11",
+ old: "/snap/some-snap/7",
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "undo-setup-snap",
+ name: "/snap/some-snap/11",
+ stype: "app",
+ },
+ }
+
+ // ensure all our tasks ran
+ c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
+ macaroon: s.user.StoreMacaroon,
+ name: "some-snap",
+ }})
+ // friendlier failure first
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Channel, Equals, "stable")
+ c.Assert(snapst.Sequence, HasLen, 1)
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "",
+ Revision: snap.R(7),
+ })
+}
+
+func (s *snapmgrTestSuite) TestUpdateSameRevision(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ })
+
+ _, err := snapstate.Update(s.state, "some-snap", "channel-for-7", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `snap "some-snap" has no updates available`)
+}
+
+func (s *snapmgrTestSuite) TestUpdateValidateRefreshesSaysNo(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ })
+
+ validateErr := errors.New("refresh control error")
+ validateRefreshes := func(st *state.State, refreshes []*snap.Info, userID int) ([]*snap.Info, error) {
+ c.Check(refreshes, HasLen, 1)
+ c.Check(refreshes[0].SnapID, Equals, "some-snap-id")
+ c.Check(refreshes[0].Revision, Equals, snap.R(11))
+ return nil, validateErr
+ }
+ // hook it up
+ snapstate.ValidateRefreshes = validateRefreshes
+
+ _, err := snapstate.Update(s.state, "some-snap", "stable", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, Equals, validateErr)
+}
+
+func (s *snapmgrTestSuite) TestUpdateValidateRefreshesSaysNoButIgnoreValidationIsSet(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ validateErr := errors.New("refresh control error")
+ validateRefreshes := func(st *state.State, refreshes []*snap.Info, userID int) ([]*snap.Info, error) {
+ return nil, validateErr
+ }
+ // hook it up
+ snapstate.ValidateRefreshes = validateRefreshes
+
+ flags := snapstate.Flags{JailMode: true, IgnoreValidation: true}
+ ts, err := snapstate.Update(s.state, "some-snap", "stable", snap.R(0), s.user.ID, flags)
+ c.Assert(err, IsNil)
+
+ var snapsup snapstate.SnapSetup
+ err = ts.Tasks()[0].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Flags, DeepEquals, flags.ForSnapSetup())
+}
+
+func (s *snapmgrTestSuite) TestSingleUpdateBlockedRevision(c *C) {
+ // single updates should *not* set the block list
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+ si11 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(11),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si7, &si11},
+ Current: si7.Revision,
+ })
+
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ c.Assert(s.fakeBackend.ops, HasLen, 1)
+ c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
+ op: "storesvc-list-refresh",
+ revno: snap.R(11),
+ cand: store.RefreshCandidate{
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ Epoch: "",
+ Channel: "some-channel",
+ },
+ })
+
+}
+
+func (s *snapmgrTestSuite) TestMultiUpdateBlockedRevision(c *C) {
+ // multi-updates should *not* set the block list
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+ si11 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(11),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si7, &si11},
+ Current: si7.Revision,
+ })
+
+ updates, _, err := snapstate.UpdateMany(s.state, []string{"some-snap"}, s.user.ID)
+ c.Assert(err, IsNil)
+ c.Check(updates, DeepEquals, []string{"some-snap"})
+
+ c.Assert(s.fakeBackend.ops, HasLen, 1)
+ c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
+ op: "storesvc-list-refresh",
+ revno: snap.R(11),
+ cand: store.RefreshCandidate{
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ },
+ })
+
+}
+
+func (s *snapmgrTestSuite) TestAllUpdateBlockedRevision(c *C) {
+ // update-all *should* set the block list
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+ si11 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(11),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si7, &si11},
+ Current: si7.Revision,
+ })
+
+ updates, _, err := snapstate.UpdateMany(s.state, nil, s.user.ID)
+ c.Check(err, IsNil)
+ c.Check(updates, HasLen, 0)
+
+ c.Assert(s.fakeBackend.ops, HasLen, 1)
+ c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
+ op: "storesvc-list-refresh",
+ cand: store.RefreshCandidate{
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ Block: []snap.Revision{snap.R(11)},
+ },
+ })
+
+}
+
+var orthogonalAutoAliasesScenarios = []struct {
+ aliasesBefore map[string][]string
+ names []string
+ retire []string
+ update bool
+ new bool
+}{
+ {nil, nil, nil, true, true},
+ {nil, []string{"some-snap"}, nil, true, false},
+ {nil, []string{"other-snap"}, nil, false, true},
+ {map[string][]string{"some-snap": {"aliasA", "aliasC"}}, []string{"some-snap"}, nil, true, false},
+ {map[string][]string{"other-snap": {"aliasB", "aliasC"}}, []string{"other-snap"}, []string{"other-snap"}, false, false},
+ {map[string][]string{"other-snap": {"aliasB", "aliasC"}}, nil, []string{"other-snap"}, true, false},
+ {map[string][]string{"other-snap": {"aliasB", "aliasC"}}, []string{"some-snap"}, nil, true, false},
+ {map[string][]string{"other-snap": {"aliasC"}}, []string{"other-snap"}, []string{"other-snap"}, false, true},
+ {map[string][]string{"other-snap": {"aliasC"}}, nil, []string{"other-snap"}, true, true},
+ {map[string][]string{"other-snap": {"aliasC"}}, []string{"some-snap"}, nil, true, false},
+ {map[string][]string{"some-snap": {"aliasB"}, "other-snap": {"aliasA"}}, []string{"some-snap"}, []string{"other-snap"}, true, false},
+ {map[string][]string{"some-snap": {"aliasB"}, "other-snap": {"aliasA"}}, nil, []string{"other-snap", "some-snap"}, true, true},
+ {map[string][]string{"some-snap": {"aliasB"}, "other-snap": {"aliasA"}}, []string{"other-snap"}, []string{"other-snap", "some-snap"}, false, true},
+ {map[string][]string{"some-snap": {"aliasB"}}, nil, []string{"some-snap"}, true, true},
+ {map[string][]string{"some-snap": {"aliasB"}}, []string{"other-snap"}, []string{"some-snap"}, false, true},
+ {map[string][]string{"some-snap": {"aliasB"}}, []string{"some-snap"}, nil, true, false},
+ {map[string][]string{"other-snap": {"aliasA"}}, nil, []string{"other-snap"}, true, true},
+ {map[string][]string{"other-snap": {"aliasA"}}, []string{"other-snap"}, []string{"other-snap"}, false, true},
+ {map[string][]string{"other-snap": {"aliasA"}}, []string{"some-snap"}, []string{"other-snap"}, true, false},
+}
+
+func (s *snapmgrTestSuite) TestUpdateManyAutoAliasesScenarios(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "other-snap", SnapID: "other-snap-id", Revision: snap.R(2)},
+ },
+ Current: snap.R(2),
+ SnapType: "app",
+ })
+
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ switch info.Name() {
+ case "some-snap":
+ return []string{"aliasA"}, nil
+ case "other-snap":
+ return []string{"aliasB"}, nil
+ }
+ return nil, nil
+ }
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
+ },
+ Current: snap.R(4),
+ SnapType: "app",
+ })
+
+ expectedAuto := func(aliases []string) map[string]string {
+ res := make(map[string]string, len(aliases))
+ for _, alias := range aliases {
+ res[alias] = "auto"
+ }
+ return res
+ }
+
+ for _, scenario := range orthogonalAutoAliasesScenarios {
+ aliases := make(map[string]map[string]string)
+ for snapName, autoAliases := range scenario.aliasesBefore {
+ statuses := make(map[string]string)
+ for _, alias := range autoAliases {
+ statuses[alias] = "auto"
+ }
+ aliases[snapName] = statuses
+ }
+ s.state.Set("aliases", aliases)
+
+ updates, tts, err := snapstate.UpdateMany(s.state, scenario.names, s.user.ID)
+ c.Check(err, IsNil)
+
+ new, retiring, err := snapstate.AutoAliasesDelta(s.state, []string{"some-snap", "other-snap"})
+ c.Assert(err, IsNil)
+
+ j := 0
+ expectedUpdatesSet := make(map[string]bool)
+ var expectedRetiring map[string]map[string]string
+ var retireTs *state.TaskSet
+ if len(scenario.retire) != 0 {
+ retireTs = tts[0]
+ j++
+ taskAliases := make(map[string]map[string]string)
+ for _, aliasTask := range retireTs.Tasks() {
+ c.Check(aliasTask.Kind(), Equals, "alias")
+ var aliases map[string]string
+ err := aliasTask.Get("aliases", &aliases)
+ c.Assert(err, IsNil)
+ snapsup, err := snapstate.TaskSnapSetup(aliasTask)
+ c.Assert(err, IsNil)
+ taskAliases[snapsup.Name()] = aliases
+ }
+ expectedRetiring = make(map[string]map[string]string)
+ for _, snapName := range scenario.retire {
+ expectedRetiring[snapName] = expectedAuto(retiring[snapName])
+ if snapName == "other-snap" && !scenario.new && !scenario.update {
+ expectedUpdatesSet["other-snap"] = true
+ }
+ }
+ c.Check(taskAliases, DeepEquals, expectedRetiring)
+ }
+ if scenario.update {
+ updateTs := tts[j]
+ j++
+ expectedUpdatesSet["some-snap"] = true
+ first := updateTs.Tasks()[0]
+ c.Check(first.Kind(), Equals, "download-snap")
+ wait := false
+ if expectedRetiring["other-snap"]["aliasA"] != "" {
+ wait = true
+ } else if expectedRetiring["some-snap"] != nil {
+ wait = true
+ }
+ if wait {
+ c.Check(first.WaitTasks(), DeepEquals, retireTs.Tasks())
+ } else {
+ c.Check(first.WaitTasks(), HasLen, 0)
+ }
+ }
+ if scenario.new {
+ newTs := tts[j]
+ j++
+ expectedUpdatesSet["other-snap"] = true
+ tasks := newTs.Tasks()
+ c.Check(tasks, HasLen, 1)
+ aliasTask := tasks[0]
+ c.Check(aliasTask.Kind(), Equals, "alias")
+ var aliases map[string]string
+ err := aliasTask.Get("aliases", &aliases)
+ c.Assert(err, IsNil)
+ c.Check(aliases, DeepEquals, expectedAuto(new["other-snap"]))
+ wait := false
+ if expectedRetiring["some-snap"]["aliasB"] != "" {
+ wait = true
+ } else if expectedRetiring["other-snap"] != nil {
+ wait = true
+ }
+ if wait {
+ c.Check(aliasTask.WaitTasks(), DeepEquals, retireTs.Tasks())
+ } else {
+ c.Check(aliasTask.WaitTasks(), HasLen, 0)
+ }
+ }
+ c.Assert(j, Equals, len(tts))
+
+ // check reported updated names
+ c.Check(len(updates) > 0, Equals, true)
+ sort.Strings(updates)
+ expectedUpdates := make([]string, 0, len(expectedUpdatesSet))
+ for x := range expectedUpdatesSet {
+ expectedUpdates = append(expectedUpdates, x)
+ }
+ sort.Strings(expectedUpdates)
+ c.Check(updates, DeepEquals, expectedUpdates)
+ }
+}
+
+func (s *snapmgrTestSuite) TestUpdateOneAutoAliasesScenarios(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "other-snap", SnapID: "other-snap-id", Revision: snap.R(2)},
+ },
+ Current: snap.R(2),
+ SnapType: "app",
+ })
+
+ snapstate.AutoAliases = func(st *state.State, info *snap.Info) ([]string, error) {
+ switch info.Name() {
+ case "some-snap":
+ return []string{"aliasA"}, nil
+ case "other-snap":
+ return []string{"aliasB"}, nil
+ }
+ return nil, nil
+ }
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
+ },
+ Current: snap.R(4),
+ SnapType: "app",
+ })
+
+ expectedAuto := func(aliases []string) map[string]string {
+ res := make(map[string]string, len(aliases))
+ for _, alias := range aliases {
+ res[alias] = "auto"
+ }
+ return res
+ }
+
+ for _, scenario := range orthogonalAutoAliasesScenarios {
+ if len(scenario.names) != 1 {
+ continue
+ }
+
+ aliases := make(map[string]map[string]string)
+ for snapName, autoAliases := range scenario.aliasesBefore {
+ statuses := make(map[string]string)
+ for _, alias := range autoAliases {
+ statuses[alias] = "auto"
+ }
+ aliases[snapName] = statuses
+ }
+ s.state.Set("aliases", aliases)
+
+ ts, err := snapstate.Update(s.state, scenario.names[0], "", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ new, retiring, err := snapstate.AutoAliasesDelta(s.state, []string{"some-snap", "other-snap"})
+ c.Assert(err, IsNil)
+
+ j := 0
+ tasks := ts.Tasks()
+ var expectedRetiring map[string]map[string]string
+ var retireTasks []*state.Task
+ if len(scenario.retire) != 0 {
+ nretire := len(scenario.retire)
+ retireTasks = tasks[:nretire]
+ j += nretire
+ taskAliases := make(map[string]map[string]string)
+ for _, aliasTask := range retireTasks {
+ c.Check(aliasTask.Kind(), Equals, "alias")
+ var aliases map[string]string
+ err := aliasTask.Get("aliases", &aliases)
+ c.Assert(err, IsNil)
+ snapsup, err := snapstate.TaskSnapSetup(aliasTask)
+ c.Assert(err, IsNil)
+ taskAliases[snapsup.Name()] = aliases
+ }
+ expectedRetiring = make(map[string]map[string]string)
+ for _, snapName := range scenario.retire {
+ expectedRetiring[snapName] = expectedAuto(retiring[snapName])
+ }
+ c.Check(taskAliases, DeepEquals, expectedRetiring)
+ }
+ if scenario.update {
+ first := tasks[j]
+ j += 14
+ c.Check(first.Kind(), Equals, "download-snap")
+ wait := false
+ if expectedRetiring["other-snap"]["aliasA"] != "" {
+ wait = true
+ } else if expectedRetiring["some-snap"] != nil {
+ wait = true
+ }
+ if wait {
+ c.Check(first.WaitTasks(), DeepEquals, retireTasks)
+ } else {
+ c.Check(first.WaitTasks(), HasLen, 0)
+ }
+ }
+ if scenario.new {
+ aliasTask := tasks[j]
+ j++
+ c.Check(aliasTask.Kind(), Equals, "alias")
+ var aliases map[string]string
+ err := aliasTask.Get("aliases", &aliases)
+ c.Assert(err, IsNil)
+ c.Check(aliases, DeepEquals, expectedAuto(new["other-snap"]))
+ wait := false
+ if expectedRetiring["some-snap"]["aliasB"] != "" {
+ wait = true
+ } else if expectedRetiring["other-snap"] != nil {
+ wait = true
+ }
+ if wait {
+ c.Check(aliasTask.WaitTasks(), DeepEquals, retireTasks)
+ } else {
+ c.Check(aliasTask.WaitTasks(), HasLen, 0)
+ }
+ }
+ c.Assert(j, Equals, len(tasks))
+ }
+}
+
+func (s *snapmgrTestSuite) TestUpdateLocalSnapFails(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ })
+
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `cannot refresh local snap "some-snap"`)
+}
+
+func (s *snapmgrTestSuite) TestUpdateDisabledUnsupported(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ })
+
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `refreshing disabled snap "some-snap" not supported`)
+}
+
+func makeTestSnap(c *C, snapYamlContent string) (snapFilePath string) {
+ return snaptest.MakeTestSnapWithFiles(c, snapYamlContent, nil)
+}
+
+func (s *snapmgrTestSuite) TestInstallFirstLocalRunThrough(c *C) {
+ // use the real thing for this one
+ snapstate.MockOpenSnapFile(backend.OpenSnapFile)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ mockSnap := makeTestSnap(c, `name: mock
+version: 1.0`)
+ chg := s.state.NewChange("install", "install a local snap")
+ ts, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "mock"}, mockSnap, "", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // ensure only local install was run, i.e. first actions are pseudo-action current
+ c.Assert(s.fakeBackend.ops.Ops(), HasLen, 9)
+ c.Check(s.fakeBackend.ops[0].op, Equals, "current")
+ c.Check(s.fakeBackend.ops[0].old, Equals, "<no-current>")
+ // and setup-snap
+ c.Check(s.fakeBackend.ops[1].op, Equals, "setup-snap")
+ c.Check(s.fakeBackend.ops[1].name, Matches, `.*/mock_1.0_all.snap`)
+ c.Check(s.fakeBackend.ops[1].revno, Equals, snap.R("x1"))
+
+ c.Check(s.fakeBackend.ops[4].op, Equals, "candidate")
+ c.Check(s.fakeBackend.ops[4].sinfo, DeepEquals, snap.SideInfo{
+ RealName: "mock",
+ Revision: snap.R(-1),
+ })
+ c.Check(s.fakeBackend.ops[5].op, Equals, "link-snap")
+ c.Check(s.fakeBackend.ops[5].name, Equals, "/snap/mock/x1")
+ c.Check(s.fakeBackend.ops[7].op, Equals, "start-snap-services")
+ c.Check(s.fakeBackend.ops[7].name, Equals, "/snap/mock/x1")
+
+ // verify snapSetup info
+ var snapsup snapstate.SnapSetup
+ task := ts.Tasks()[0]
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
+ SnapPath: mockSnap,
+ SideInfo: snapsup.SideInfo,
+ })
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "mock",
+ Revision: snap.R(-1),
+ })
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "mock", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "mock",
+ Channel: "",
+ Revision: snap.R(-1),
+ })
+ c.Assert(snapst.LocalRevision(), Equals, snap.R(-1))
+}
+
+func (s *snapmgrTestSuite) TestInstallSubsequentLocalRunThrough(c *C) {
+ // use the real thing for this one
+ snapstate.MockOpenSnapFile(backend.OpenSnapFile)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "mock", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "mock", Revision: snap.R(-2)},
+ },
+ Current: snap.R(-2),
+ })
+
+ mockSnap := makeTestSnap(c, `name: mock
+version: 1.0`)
+ chg := s.state.NewChange("install", "install a local snap")
+ ts, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "mock"}, mockSnap, "", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ ops := s.fakeBackend.ops
+ // ensure only local install was run, i.e. first action is pseudo-action current
+ c.Assert(ops, HasLen, 12)
+ c.Check(ops[0].op, Equals, "current")
+ c.Check(ops[0].old, Equals, "/snap/mock/x2")
+ // and setup-snap
+ c.Check(ops[1].op, Equals, "setup-snap")
+ c.Check(ops[1].name, Matches, `.*/mock_1.0_all.snap`)
+ c.Check(ops[1].revno, Equals, snap.R("x3"))
+ // and cleanup
+ c.Check(ops[len(ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "mock",
+ revno: snap.R("x3"),
+ })
+
+ c.Check(ops[2].op, Equals, "stop-snap-services")
+ c.Check(ops[2].name, Equals, "/snap/mock/x2")
+
+ c.Check(ops[4].op, Equals, "unlink-snap")
+ c.Check(ops[4].name, Equals, "/snap/mock/x2")
+
+ c.Check(ops[5].op, Equals, "copy-data")
+ c.Check(ops[5].name, Equals, "/snap/mock/x3")
+ c.Check(ops[5].old, Equals, "/snap/mock/x2")
+
+ c.Check(ops[6].op, Equals, "setup-profiles:Doing")
+ c.Check(ops[6].name, Equals, "mock")
+ c.Check(ops[6].revno, Equals, snap.R(-3))
+
+ c.Check(ops[7].op, Equals, "candidate")
+ c.Check(ops[7].sinfo, DeepEquals, snap.SideInfo{
+ RealName: "mock",
+ Revision: snap.R(-3),
+ })
+ c.Check(ops[8].op, Equals, "link-snap")
+ c.Check(ops[8].name, Equals, "/snap/mock/x3")
+ c.Check(ops[10].op, Equals, "start-snap-services")
+ c.Check(ops[10].name, Equals, "/snap/mock/x3")
+
+ // verify snapSetup info
+ var snapsup snapstate.SnapSetup
+ task := ts.Tasks()[0]
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
+ SnapPath: mockSnap,
+ SideInfo: snapsup.SideInfo,
+ })
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "mock",
+ Revision: snap.R(-3),
+ })
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "mock", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence, HasLen, 2)
+ c.Assert(snapst.CurrentSideInfo(), DeepEquals, &snap.SideInfo{
+ RealName: "mock",
+ Channel: "",
+ Revision: snap.R(-3),
+ })
+ c.Assert(snapst.LocalRevision(), Equals, snap.R(-3))
+}
+
+func (s *snapmgrTestSuite) TestInstallOldSubsequentLocalRunThrough(c *C) {
+ // use the real thing for this one
+ snapstate.MockOpenSnapFile(backend.OpenSnapFile)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "mock", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "mock", Revision: snap.R(100001)},
+ },
+ Current: snap.R(100001),
+ })
+
+ mockSnap := makeTestSnap(c, `name: mock
+version: 1.0`)
+ chg := s.state.NewChange("install", "install a local snap")
+ ts, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "mock"}, mockSnap, "", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // ensure only local install was run, i.e. first action is pseudo-action current
+ ops := s.fakeBackend.ops
+ c.Assert(ops, HasLen, 12)
+ c.Check(ops[0].op, Equals, "current")
+ c.Check(ops[0].old, Equals, "/snap/mock/100001")
+ // and setup-snap
+ c.Check(ops[1].op, Equals, "setup-snap")
+ c.Check(ops[1].name, Matches, `.*/mock_1.0_all.snap`)
+ c.Check(ops[1].revno, Equals, snap.R("x1"))
+ // and cleanup
+ c.Check(ops[len(ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "mock",
+ revno: snap.R("x1"),
+ })
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "mock", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence, HasLen, 2)
+ c.Assert(snapst.CurrentSideInfo(), DeepEquals, &snap.SideInfo{
+ RealName: "mock",
+ Channel: "",
+ Revision: snap.R(-1),
+ })
+ c.Assert(snapst.LocalRevision(), Equals, snap.R(-1))
+}
+
+func (s *snapmgrTestSuite) TestInstallPathWithMetadataRunThrough(c *C) {
+ // use the real thing for this one
+ snapstate.MockOpenSnapFile(backend.OpenSnapFile)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ someSnap := makeTestSnap(c, `name: orig-name
+version: 1.0`)
+ chg := s.state.NewChange("install", "install a local snap")
+
+ si := &snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Revision: snap.R(42),
+ }
+ ts, err := snapstate.InstallPath(s.state, si, someSnap, "", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // ensure only local install was run, i.e. first actions are pseudo-action current
+ c.Assert(s.fakeBackend.ops.Ops(), HasLen, 9)
+ c.Check(s.fakeBackend.ops[0].op, Equals, "current")
+ c.Check(s.fakeBackend.ops[0].old, Equals, "<no-current>")
+ // and setup-snap
+ c.Check(s.fakeBackend.ops[1].op, Equals, "setup-snap")
+ c.Check(s.fakeBackend.ops[1].name, Matches, `.*/orig-name_1.0_all.snap`)
+ c.Check(s.fakeBackend.ops[1].revno, Equals, snap.R(42))
+
+ c.Check(s.fakeBackend.ops[4].op, Equals, "candidate")
+ c.Check(s.fakeBackend.ops[4].sinfo, DeepEquals, *si)
+ c.Check(s.fakeBackend.ops[5].op, Equals, "link-snap")
+ c.Check(s.fakeBackend.ops[5].name, Equals, "/snap/some-snap/42")
+ c.Check(s.fakeBackend.ops[7].op, Equals, "start-snap-services")
+ c.Check(s.fakeBackend.ops[7].name, Equals, "/snap/some-snap/42")
+
+ // verify snapSetup info
+ var snapsup snapstate.SnapSetup
+ task := ts.Tasks()[0]
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
+ SnapPath: someSnap,
+ SideInfo: snapsup.SideInfo,
+ })
+ c.Assert(snapsup.SideInfo, DeepEquals, si)
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Channel, Equals, "")
+ c.Assert(snapst.Sequence[0], DeepEquals, si)
+ c.Assert(snapst.LocalRevision().Unset(), Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("remove", "remove a snap")
+ ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0))
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Check(len(s.fakeBackend.ops), Equals, 9)
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ {
+ op: "remove-snap-data",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-common-data",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-files",
+ name: "/snap/some-snap/7",
+ stype: "app",
+ },
+ {
+ op: "discard-namespace",
+ name: "some-snap",
+ },
+ {
+ op: "discard-conns:Doing",
+ name: "some-snap",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Check(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snapSetup info
+ tasks := ts.Tasks()
+ for _, t := range tasks {
+ snapsup, err := snapstate.TaskSnapSetup(t)
+ c.Assert(err, IsNil)
+
+ var expSnapSetup *snapstate.SnapSetup
+ if t.Kind() == "discard-conns" || t.Kind() == "clear-aliases" {
+ expSnapSetup = &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "some-snap",
+ },
+ }
+ } else {
+ expSnapSetup = &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ },
+ }
+ }
+ c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind()))
+ }
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+}
+
+func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) {
+ si3 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(3),
+ }
+
+ si5 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(5),
+ }
+
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si5, &si3, &si7},
+ Current: si7.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("remove", "remove a snap")
+ ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0))
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ {
+ op: "remove-snap-data",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-files",
+ name: "/snap/some-snap/7",
+ stype: "app",
+ },
+ {
+ op: "remove-snap-data",
+ name: "/snap/some-snap/3",
+ },
+ {
+ op: "remove-snap-files",
+ name: "/snap/some-snap/3",
+ stype: "app",
+ },
+ {
+ op: "remove-snap-data",
+ name: "/snap/some-snap/5",
+ },
+ {
+ op: "remove-snap-common-data",
+ name: "/snap/some-snap/5",
+ },
+ {
+ op: "remove-snap-files",
+ name: "/snap/some-snap/5",
+ stype: "app",
+ },
+ {
+ op: "discard-namespace",
+ name: "some-snap",
+ },
+ {
+ op: "discard-conns:Doing",
+ name: "some-snap",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snapSetup info
+ tasks := ts.Tasks()
+ revnos := []snap.Revision{{N: 7}, {N: 3}, {N: 5}}
+ whichRevno := 0
+ for _, t := range tasks {
+ snapsup, err := snapstate.TaskSnapSetup(t)
+ c.Assert(err, IsNil)
+
+ var expSnapSetup *snapstate.SnapSetup
+ if t.Kind() == "discard-conns" || t.Kind() == "clear-aliases" {
+ expSnapSetup = &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "some-snap",
+ },
+ }
+ } else {
+ expSnapSetup = &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "some-snap",
+ Revision: revnos[whichRevno],
+ },
+ }
+ }
+
+ c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind()))
+
+ if t.Kind() == "discard-snap" {
+ whichRevno++
+ }
+ }
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+}
+
+func (s *snapmgrTestSuite) TestRemoveOneRevisionRunThrough(c *C) {
+ si3 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(3),
+ }
+
+ si5 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(5),
+ }
+
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si5, &si3, &si7},
+ Current: si7.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("remove", "remove a snap")
+ ts, err := snapstate.Remove(s.state, "some-snap", snap.R(3))
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Check(len(s.fakeBackend.ops), Equals, 2)
+ expected := fakeOps{
+ {
+ op: "remove-snap-data",
+ name: "/snap/some-snap/3",
+ },
+ {
+ op: "remove-snap-files",
+ name: "/snap/some-snap/3",
+ stype: "app",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snapSetup info
+ tasks := ts.Tasks()
+ for _, t := range tasks {
+ snapsup, err := snapstate.TaskSnapSetup(t)
+ c.Assert(err, IsNil)
+
+ expSnapSetup := &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(3),
+ },
+ }
+
+ c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind()))
+ }
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Sequence, HasLen, 2)
+}
+
+func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("remove", "remove a snap")
+ ts, err := snapstate.Remove(s.state, "some-snap", snap.R(2))
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Check(len(s.fakeBackend.ops), Equals, 5)
+ expected := fakeOps{
+ {
+ op: "remove-snap-data",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "remove-snap-common-data",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "remove-snap-files",
+ name: "/snap/some-snap/2",
+ stype: "app",
+ },
+ {
+ op: "discard-namespace",
+ name: "some-snap",
+ },
+ {
+ op: "discard-conns:Doing",
+ name: "some-snap",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snapSetup info
+ tasks := ts.Tasks()
+ for _, t := range tasks {
+ snapsup, err := snapstate.TaskSnapSetup(t)
+ c.Assert(err, IsNil)
+
+ expSnapSetup := &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "some-snap",
+ },
+ }
+ if t.Kind() != "discard-conns" && t.Kind() != "clear-aliases" {
+ expSnapSetup.SideInfo.Revision = snap.R(2)
+ }
+
+ c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind()))
+ }
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, Equals, state.ErrNoState)
+}
+
+func (s *snapmgrTestSuite) TestRemoveCurrentActiveRevisionRefused(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ _, err := snapstate.Remove(s.state, "some-snap", snap.R(2))
+
+ c.Check(err, ErrorMatches, `cannot remove active revision 2 of snap "some-snap"`)
+}
+
+func (s *snapmgrTestSuite) TestRemoveCurrentRevisionOfSeveralRefused(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ _, err := snapstate.Remove(s.state, "some-snap", snap.R(2))
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, `cannot remove active revision 2 of snap "some-snap" (revert first?)`)
+}
+
+func (s *snapmgrTestSuite) TestRemoveMissingRevisionRefused(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ _, err := snapstate.Remove(s.state, "some-snap", snap.R(1))
+
+ c.Check(err, ErrorMatches, `revision 1 of snap "some-snap" is not installed`)
+}
+
+func (s *snapmgrTestSuite) TestRemoveRefused(c *C) {
+ si := snap.SideInfo{
+ RealName: "gadget",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "gadget", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ SnapType: "app",
+ })
+
+ _, err := snapstate.Remove(s.state, "gadget", snap.R(0))
+
+ c.Check(err, ErrorMatches, `snap "gadget" is not removable`)
+}
+
+func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)},
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)},
+ },
+ Current: snap.R(4),
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("update", "update a snap")
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // ensure garbage collection runs as the last tasks
+ ops := s.fakeBackend.ops
+ c.Assert(ops[len(ops)-8], DeepEquals, fakeOp{
+ op: "link-snap",
+ name: "/snap/some-snap/11",
+ })
+ c.Assert(ops[len(ops)-6], DeepEquals, fakeOp{
+ op: "start-snap-services",
+ name: "/snap/some-snap/11",
+ })
+ c.Assert(ops[len(ops)-5], DeepEquals, fakeOp{
+ op: "remove-snap-data",
+ name: "/snap/some-snap/1",
+ })
+ c.Assert(ops[len(ops)-4], DeepEquals, fakeOp{
+ op: "remove-snap-files",
+ name: "/snap/some-snap/1",
+ stype: "app",
+ })
+ c.Assert(ops[len(ops)-3], DeepEquals, fakeOp{
+ op: "remove-snap-data",
+ name: "/snap/some-snap/2",
+ })
+ c.Assert(ops[len(ops)-2], DeepEquals, fakeOp{
+ op: "remove-snap-files",
+ name: "/snap/some-snap/2",
+ stype: "app",
+ })
+ c.Assert(ops[len(ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(11),
+ })
+
+}
+
+func (s *snapmgrTestSuite) TestRevertNoRevertAgain(c *C) {
+ siNew := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(77),
+ }
+
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &siNew},
+ Current: snap.R(7),
+ })
+
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, ErrorMatches, "no revision to revert to")
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestRevertNothingToRevertTo(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ })
+
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, ErrorMatches, "no revision to revert to")
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestRevertToRevisionNoValidVersion(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+ si2 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(77),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &si2},
+ Current: snap.R(77),
+ })
+
+ ts, err := snapstate.RevertToRevision(s.state, "some-snap", snap.R("99"), snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `cannot find revision 99 for snap "some-snap"`)
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestRevertToRevisionAlreadyCurrent(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+ si2 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(77),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &si2},
+ Current: snap.R(77),
+ })
+
+ ts, err := snapstate.RevertToRevision(s.state, "some-snap", snap.R("77"), snapstate.Flags{})
+ c.Assert(err, ErrorMatches, `already on requested revision`)
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestRevertRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+ siOld := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&siOld, &si},
+ Current: si.Revision,
+ })
+
+ chg := s.state.NewChange("revert", "revert a snap backwards")
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(2),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/2",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify that the R(2) version is active now and R(7) is still there
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Current, Equals, snap.R(2))
+ c.Assert(snapst.Sequence, HasLen, 2)
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "",
+ Revision: snap.R(2),
+ })
+ c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "",
+ Revision: snap.R(7),
+ })
+ c.Assert(snapst.Block(), DeepEquals, []snap.Revision{snap.R(7)})
+}
+
+func (s *snapmgrTestSuite) TestRevertWithLocalRevisionRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(-7),
+ }
+ siOld := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(-2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&siOld, &si},
+ Current: si.Revision,
+ })
+
+ chg := s.state.NewChange("revert", "revert a snap backwards")
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Assert(s.fakeBackend.ops, HasLen, 8)
+
+ // verify that LocalRevision is still -7
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.LocalRevision(), Equals, snap.R(-7))
+}
+
+func (s *snapmgrTestSuite) TestRevertToRevisionNewVersion(c *C) {
+ siNew := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ SnapID: "october",
+ }
+
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ SnapID: "october",
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &siNew},
+ Current: snap.R(2),
+ Channel: "edge",
+ })
+
+ chg := s.state.NewChange("revert", "revert a snap forward")
+ ts, err := snapstate.RevertToRevision(s.state, "some-snap", snap.R(7), snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ {
+ op: "candidate",
+ sinfo: siNew,
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify that the R(7) version is active now
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Check(snapst.Active, Equals, true)
+ c.Check(snapst.Current, Equals, snap.R(7))
+ c.Check(snapst.Sequence, HasLen, 2)
+ c.Check(snapst.Channel, Equals, "edge")
+ c.Check(snapst.CurrentSideInfo(), DeepEquals, &siNew)
+
+ c.Check(snapst.Block(), HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(1),
+ }
+ si2 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &si2},
+ Current: si2.Revision,
+ })
+
+ chg := s.state.NewChange("revert", "revert a snap")
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(1),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(1),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/1",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/1",
+ },
+ // undoing everything from here down...
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/1",
+ },
+ {
+ op: "matching-aliases",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/1",
+ },
+ {
+ op: "setup-profiles:Undoing",
+ name: "some-snap",
+ revno: snap.R(1),
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/2",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Check(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence, HasLen, 2)
+ c.Assert(snapst.Current, Equals, si2.Revision)
+}
+
+func (s *snapmgrTestSuite) TestRevertUndoRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(1),
+ }
+ si2 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(2),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si, &si2},
+ Current: si2.Revision,
+ })
+
+ chg := s.state.NewChange("revert", "install a revert")
+ ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.fakeBackend.linkSnapFailTrigger = "/snap/some-snap/1"
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(1),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(1),
+ },
+ },
+ {
+ op: "link-snap.failed",
+ name: "/snap/some-snap/1",
+ },
+ // undo stuff here
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/1",
+ },
+ {
+ op: "setup-profiles:Undoing",
+ name: "some-snap",
+ revno: snap.R(1),
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/2",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/2",
+ },
+ }
+
+ // ensure all our tasks ran
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // verify snaps in the system state
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Sequence, HasLen, 2)
+ c.Assert(snapst.Current, Equals, snap.R(2))
+}
+
+func (s *snapmgrTestSuite) TestEnableDoesNotEnableAgain(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{&si},
+ Current: snap.R(7),
+ Active: true,
+ })
+
+ ts, err := snapstate.Enable(s.state, "some-snap")
+ c.Assert(err, ErrorMatches, `snap "some-snap" already enabled`)
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestEnableRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ Channel: "edge",
+ SnapID: "foo",
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ Active: false,
+ Channel: "edge",
+ })
+
+ chg := s.state.NewChange("enable", "enable a snap")
+ ts, err := snapstate.Enable(s.state, "some-snap")
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "candidate",
+ sinfo: si,
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, true)
+ info, err := snapst.CurrentInfo()
+ c.Assert(err, IsNil)
+ c.Assert(info.Channel, Equals, "edge")
+ c.Assert(info.SnapID, Equals, "foo")
+}
+
+func (s *snapmgrTestSuite) TestDisableRunThrough(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{&si},
+ Current: si.Revision,
+ Active: true,
+ })
+
+ chg := s.state.NewChange("disable", "disable a snap")
+ ts, err := snapstate.Disable(s.state, "some-snap")
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/7",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+
+ c.Assert(snapst.Active, Equals, false)
+}
+
+func (s *snapmgrTestSuite) TestDisableDoesNotEnableAgain(c *C) {
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{&si},
+ Current: snap.R(7),
+ Active: false,
+ })
+
+ ts, err := snapstate.Disable(s.state, "some-snap")
+ c.Assert(err, ErrorMatches, `snap "some-snap" already disabled`)
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestUndoMountSnapFailsInCopyData(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "install a snap")
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.fakeBackend.copySnapDataFailTrigger = "/snap/some-snap/11"
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ expected := fakeOps{
+ {
+ op: "storesvc-snap",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "storesvc-download",
+ name: "some-snap",
+ },
+ {
+ op: "validate-snap:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "current",
+ old: "<no-current>",
+ },
+ {
+ op: "open-snap-file",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "snapIDsnapidsnapidsnapidsnapidsn",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "setup-snap",
+ name: "/var/lib/snapd/snaps/some-snap_11.snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "copy-data.failed",
+ name: "/snap/some-snap/11",
+ old: "<no-old>",
+ },
+ {
+ op: "undo-setup-snap",
+ name: "/snap/some-snap/11",
+ stype: "app",
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+}
+
+type snapmgrQuerySuite struct {
+ st *state.State
+}
+
+var _ = Suite(&snapmgrQuerySuite{})
+
+func (s *snapmgrQuerySuite) SetUpTest(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ s.st = st
+
+ dirs.SetRootDir(c.MkDir())
+
+ // Write a snap.yaml with fake name
+ sideInfo11 := &snap.SideInfo{RealName: "name1", Revision: snap.R(11), EditedSummary: "s11"}
+ sideInfo12 := &snap.SideInfo{RealName: "name1", Revision: snap.R(12), EditedSummary: "s12"}
+ snaptest.MockSnap(c, `
+name: name0
+version: 1.1
+description: |
+ Lots of text`, "", sideInfo11)
+ snaptest.MockSnap(c, `
+name: name0
+version: 1.2
+description: |
+ Lots of text`, "", sideInfo12)
+ snapstate.Set(st, "name1", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfo11, sideInfo12},
+ Current: sideInfo12.Revision,
+ SnapType: "app",
+ })
+
+ // have also a snap being installed
+ /*
+ snapstate.Set(st, "installing", &snapstate.SnapState{
+ Candidate: &snap.SideInfo{RealName: "installing", Revision: snap.R(1)},
+ })
+ */
+}
+
+func (s *snapmgrQuerySuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *snapmgrQuerySuite) TestInfo(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ info, err := snapstate.Info(st, "name1", snap.R(11))
+ c.Assert(err, IsNil)
+
+ c.Check(info.Name(), Equals, "name1")
+ c.Check(info.Revision, Equals, snap.R(11))
+ c.Check(info.Summary(), Equals, "s11")
+ c.Check(info.Version, Equals, "1.1")
+ c.Check(info.Description(), Equals, "Lots of text")
+}
+
+func (s *snapmgrQuerySuite) TestSnapStateCurrentInfo(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, "name1", &snapst)
+ c.Assert(err, IsNil)
+
+ info, err := snapst.CurrentInfo()
+ c.Assert(err, IsNil)
+
+ c.Check(info.Name(), Equals, "name1")
+ c.Check(info.Revision, Equals, snap.R(12))
+ c.Check(info.Summary(), Equals, "s12")
+ c.Check(info.Version, Equals, "1.2")
+ c.Check(info.Description(), Equals, "Lots of text")
+}
+
+func (s *snapmgrQuerySuite) TestSnapStateCurrentInfoErrNoCurrent(c *C) {
+ snapst := new(snapstate.SnapState)
+ _, err := snapst.CurrentInfo()
+ c.Assert(err, Equals, snapstate.ErrNoCurrent)
+
+}
+
+func (s *snapmgrQuerySuite) TestCurrentInfo(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ info, err := snapstate.CurrentInfo(st, "name1")
+ c.Assert(err, IsNil)
+
+ c.Check(info.Name(), Equals, "name1")
+ c.Check(info.Revision, Equals, snap.R(12))
+}
+
+func (s *snapmgrQuerySuite) TestCurrentInfoAbsent(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ _, err := snapstate.CurrentInfo(st, "absent")
+ c.Assert(err, ErrorMatches, `cannot find snap "absent"`)
+}
+
+func (s *snapmgrQuerySuite) TestActiveInfos(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ infos, err := snapstate.ActiveInfos(st)
+ c.Assert(err, IsNil)
+
+ c.Check(infos, HasLen, 1)
+
+ c.Check(infos[0].Name(), Equals, "name1")
+ c.Check(infos[0].Revision, Equals, snap.R(12))
+ c.Check(infos[0].Summary(), Equals, "s12")
+ c.Check(infos[0].Version, Equals, "1.2")
+ c.Check(infos[0].Description(), Equals, "Lots of text")
+}
+
+func (s *snapmgrQuerySuite) TestTypeInfo(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ for _, x := range []struct {
+ snapName string
+ snapType snap.Type
+ getInfo func(*state.State) (*snap.Info, error)
+ }{
+ {
+ snapName: "gadget",
+ snapType: snap.TypeGadget,
+ getInfo: snapstate.GadgetInfo,
+ },
+ {
+ snapName: "core",
+ snapType: snap.TypeOS,
+ getInfo: snapstate.CoreInfo,
+ },
+ {
+ snapName: "kernel",
+ snapType: snap.TypeKernel,
+ getInfo: snapstate.KernelInfo,
+ },
+ } {
+ _, err := x.getInfo(st)
+ c.Assert(err, Equals, state.ErrNoState)
+
+ sideInfo := &snap.SideInfo{
+ RealName: x.snapName,
+ Revision: snap.R(2),
+ }
+ snaptest.MockSnap(c, fmt.Sprintf("name: %q\ntype: %q\nversion: %q\n", x.snapName, x.snapType, x.snapName), "", sideInfo)
+ snapstate.Set(st, x.snapName, &snapstate.SnapState{
+ SnapType: string(x.snapType),
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfo},
+ Current: sideInfo.Revision,
+ })
+
+ info, err := x.getInfo(st)
+ c.Assert(err, IsNil)
+
+ c.Check(info.Name(), Equals, x.snapName)
+ c.Check(info.Revision, Equals, snap.R(2))
+ c.Check(info.Version, Equals, x.snapName)
+ c.Check(info.Type, Equals, x.snapType)
+ }
+}
+
+func (s *snapmgrQuerySuite) TestPreviousSideInfo(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, "name1", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.CurrentSideInfo(), NotNil)
+ c.Assert(snapst.CurrentSideInfo().Revision, Equals, snap.R(12))
+ c.Assert(snapstate.PreviousSideInfo(&snapst), NotNil)
+ c.Assert(snapstate.PreviousSideInfo(&snapst).Revision, Equals, snap.R(11))
+}
+
+func (s *snapmgrQuerySuite) TestPreviousSideInfoNoCurrent(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ snapst := &snapstate.SnapState{}
+ c.Assert(snapstate.PreviousSideInfo(snapst), IsNil)
+}
+
+func (s *snapmgrQuerySuite) TestAll(c *C) {
+ st := s.st
+ st.Lock()
+ defer st.Unlock()
+
+ snapStates, err := snapstate.All(st)
+ c.Assert(err, IsNil)
+ c.Assert(snapStates, HasLen, 1)
+
+ snapst := snapStates["name1"]
+ c.Assert(snapst, NotNil)
+
+ c.Check(snapst.Active, Equals, true)
+ c.Check(snapst.CurrentSideInfo(), NotNil)
+
+ info12, err := snap.ReadInfo("name1", snapst.CurrentSideInfo())
+ c.Assert(err, IsNil)
+
+ c.Check(info12.Name(), Equals, "name1")
+ c.Check(info12.Revision, Equals, snap.R(12))
+ c.Check(info12.Summary(), Equals, "s12")
+ c.Check(info12.Version, Equals, "1.2")
+ c.Check(info12.Description(), Equals, "Lots of text")
+
+ info11, err := snap.ReadInfo("name1", snapst.Sequence[0])
+ c.Assert(err, IsNil)
+
+ c.Check(info11.Name(), Equals, "name1")
+ c.Check(info11.Revision, Equals, snap.R(11))
+ c.Check(info11.Version, Equals, "1.1")
+}
+
+func (s *snapmgrQuerySuite) TestAllEmptyAndEmptyNormalisation(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ snapStates, err := snapstate.All(st)
+ c.Assert(err, IsNil)
+ c.Check(snapStates, HasLen, 0)
+
+ snapstate.Set(st, "foo", nil)
+
+ snapStates, err = snapstate.All(st)
+ c.Assert(err, IsNil)
+ c.Check(snapStates, HasLen, 0)
+
+ snapstate.Set(st, "foo", &snapstate.SnapState{})
+
+ snapStates, err = snapstate.All(st)
+ c.Assert(err, IsNil)
+ c.Check(snapStates, HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestTrySetsTryMode(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // make mock try dir
+ tryYaml := filepath.Join(c.MkDir(), "meta", "snap.yaml")
+ err := os.MkdirAll(filepath.Dir(tryYaml), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(tryYaml, []byte("name: foo\nversion: 1.0"), 0644)
+ c.Assert(err, IsNil)
+
+ chg := s.state.NewChange("try", "try snap")
+ ts, err := snapstate.TryPath(s.state, "foo", filepath.Dir(filepath.Dir(tryYaml)), snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // verify snap is in TryMode
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.TryMode, Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestTryUndoRemovesTryFlag(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // simulate existing state for foo
+ var snapst snapstate.SnapState
+ snapst.Sequence = []*snap.SideInfo{
+ {
+ RealName: "foo",
+ Revision: snap.R(23),
+ },
+ }
+ snapst.Current = snap.R(23)
+ snapstate.Set(s.state, "foo", &snapst)
+ c.Check(snapst.TryMode, Equals, false)
+
+ chg := s.state.NewChange("try", "try snap")
+ ts, err := snapstate.TryPath(s.state, "foo", c.MkDir(), snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ last := ts.Tasks()[len(ts.Tasks())-1]
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ chg.AddTask(terr)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ // verify snap is not in try mode, the state got undone
+ err = snapstate.Get(s.state, "foo", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.TryMode, Equals, false)
+}
+
+type snapStateSuite struct{}
+
+var _ = Suite(&snapStateSuite{})
+
+func (s *snapStateSuite) TestSnapStateDevMode(c *C) {
+ snapst := &snapstate.SnapState{}
+ c.Check(snapst.DevMode, Equals, false)
+ snapst.Flags.DevMode = true
+ c.Check(snapst.DevMode, Equals, true)
+}
+
+func (s *snapStateSuite) TestSnapStateType(c *C) {
+ snapst := &snapstate.SnapState{}
+ _, err := snapst.Type()
+ c.Check(err, ErrorMatches, "snap type unset")
+
+ snapst.SetType(snap.TypeKernel)
+ typ, err := snapst.Type()
+ c.Assert(err, IsNil)
+ c.Check(typ, Equals, snap.TypeKernel)
+}
+
+func (s *snapStateSuite) TestCurrentSideInfoEmpty(c *C) {
+ var snapst snapstate.SnapState
+ c.Check(snapst.CurrentSideInfo(), IsNil)
+ c.Check(snapst.Current.Unset(), Equals, true)
+}
+
+func (s *snapStateSuite) TestCurrentSideInfoSimple(c *C) {
+ si1 := &snap.SideInfo{Revision: snap.R(1)}
+ snapst := snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si1},
+ Current: snap.R(1),
+ }
+ c.Check(snapst.CurrentSideInfo(), DeepEquals, si1)
+}
+
+func (s *snapStateSuite) TestCurrentSideInfoInOrder(c *C) {
+ si1 := &snap.SideInfo{Revision: snap.R(1)}
+ si2 := &snap.SideInfo{Revision: snap.R(2)}
+ snapst := snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si1, si2},
+ Current: snap.R(2),
+ }
+ c.Check(snapst.CurrentSideInfo(), DeepEquals, si2)
+}
+
+func (s *snapStateSuite) TestCurrentSideInfoOutOfOrder(c *C) {
+ si1 := &snap.SideInfo{Revision: snap.R(1)}
+ si2 := &snap.SideInfo{Revision: snap.R(2)}
+ snapst := snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si1, si2},
+ Current: snap.R(1),
+ }
+ c.Check(snapst.CurrentSideInfo(), DeepEquals, si1)
+}
+
+func (s *snapStateSuite) TestCurrentSideInfoInconsistent(c *C) {
+ snapst := snapstate.SnapState{
+ Sequence: []*snap.SideInfo{
+ {Revision: snap.R(1)},
+ },
+ }
+ c.Check(func() { snapst.CurrentSideInfo() }, PanicMatches, `snapst.Current and snapst.Sequence out of sync:.*`)
+}
+
+func (s *snapStateSuite) TestCurrentSideInfoInconsistentWithCurrent(c *C) {
+ snapst := snapstate.SnapState{Current: snap.R(17)}
+ c.Check(func() { snapst.CurrentSideInfo() }, PanicMatches, `cannot find snapst.Current in the snapst.Sequence`)
+}
+
+type snapSetupSuite struct{}
+
+var _ = Suite(&snapSetupSuite{})
+
+type canRemoveSuite struct{}
+
+var _ = Suite(&canRemoveSuite{})
+
+func (s *canRemoveSuite) TestAppAreAlwaysOKToRemove(c *C) {
+ info := &snap.Info{
+ Type: snap.TypeApp,
+ }
+ info.RealName = "foo"
+
+ c.Check(snapstate.CanRemove(info, false), Equals, true)
+ c.Check(snapstate.CanRemove(info, true), Equals, true)
+}
+
+func (s *canRemoveSuite) TestActiveGadgetsAreNotOK(c *C) {
+ info := &snap.Info{
+ Type: snap.TypeGadget,
+ }
+ info.RealName = "foo"
+
+ c.Check(snapstate.CanRemove(info, false), Equals, true)
+ c.Check(snapstate.CanRemove(info, true), Equals, false)
+}
+
+func (s *canRemoveSuite) TestActiveOSAndKernelAreNotOK(c *C) {
+ os := &snap.Info{
+ Type: snap.TypeOS,
+ }
+ os.RealName = "os"
+ kernel := &snap.Info{
+ Type: snap.TypeKernel,
+ }
+ kernel.RealName = "krnl"
+
+ c.Check(snapstate.CanRemove(os, false), Equals, true)
+ c.Check(snapstate.CanRemove(os, true), Equals, false)
+
+ c.Check(snapstate.CanRemove(kernel, false), Equals, true)
+ c.Check(snapstate.CanRemove(kernel, true), Equals, false)
+}
+
+func revs(seq []*snap.SideInfo) []int {
+ revs := make([]int, len(seq))
+ for i, si := range seq {
+ revs[i] = si.Revision.N
+ }
+
+ return revs
+}
+
+type opSeqOpts struct {
+ revert bool
+ fail bool
+ before []int
+ current int
+ via int
+ after []int
+}
+
+// build a SnapState with a revision sequence given by `before` and a
+// current revision of `current`. Then refresh --revision via. Then
+// check the revision sequence is as in `after`.
+func (s *snapmgrTestSuite) testOpSequence(c *C, opts *opSeqOpts) (*snapstate.SnapState, *state.TaskSet) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ seq := make([]*snap.SideInfo, len(opts.before))
+ for i, n := range opts.before {
+ seq[i] = &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(n)}
+ }
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: seq,
+ Current: snap.R(opts.current),
+ SnapType: "app",
+ })
+
+ var chg *state.Change
+ var ts *state.TaskSet
+ var err error
+ if opts.revert {
+ chg = s.state.NewChange("revert", "revert a snap")
+ ts, err = snapstate.RevertToRevision(s.state, "some-snap", snap.R(opts.via), snapstate.Flags{})
+ } else {
+ chg = s.state.NewChange("refresh", "refresh a snap")
+ ts, err = snapstate.Update(s.state, "some-snap", "", snap.R(opts.via), s.user.ID, snapstate.Flags{})
+ }
+ c.Assert(err, IsNil)
+ if opts.fail {
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ if len(last.Lanes()) > 0 {
+ lanes := last.Lanes()
+ // sanity
+ c.Assert(lanes, HasLen, 1)
+ terr.JoinLane(lanes[0])
+ }
+ chg.AddTask(terr)
+ }
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(revs(snapst.Sequence), DeepEquals, opts.after)
+
+ return &snapst, ts
+}
+
+func (s *snapmgrTestSuite) testUpdateSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = false
+ snapst, ts := s.testOpSequence(c, opts)
+ // update always ends with current==seq[-1]==via:
+ c.Check(snapst.Current.N, Equals, opts.after[len(opts.after)-1])
+ c.Check(snapst.Current.N, Equals, opts.via)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 1)
+ c.Check(s.fakeBackend.ops.First("copy-data"), DeepEquals, &fakeOp{
+ op: "copy-data",
+ name: fmt.Sprintf("/snap/some-snap/%d", opts.via),
+ old: fmt.Sprintf("/snap/some-snap/%d", opts.current),
+ })
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testUpdateFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = false
+ opts.after = opts.before
+ s.fakeBackend.linkSnapFailTrigger = fmt.Sprintf("/snap/some-snap/%d", opts.via)
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed update will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ ops := s.fakeBackend.ops
+ c.Check(ops.Count("copy-data"), Equals, 1)
+ do := ops.First("copy-data")
+
+ c.Check(ops.Count("undo-copy-snap-data"), Equals, 1)
+ undo := ops.First("undo-copy-snap-data")
+
+ do.op = undo.op
+ c.Check(do, DeepEquals, undo) // i.e. they only differed in the op
+
+ return ts
+}
+
+// testTotal*Failure fails *after* link-snap
+func (s *snapmgrTestSuite) testTotalUpdateFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = false
+ opts.fail = true
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed update will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ ops := s.fakeBackend.ops
+ c.Check(ops.Count("copy-data"), Equals, 1)
+ do := ops.First("copy-data")
+
+ c.Check(ops.Count("undo-copy-snap-data"), Equals, 1)
+ undo := ops.First("undo-copy-snap-data")
+
+ do.op = undo.op
+ c.Check(do, DeepEquals, undo) // i.e. they only differed in the op
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testRevertSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = true
+ opts.after = opts.before
+ snapst, ts := s.testOpSequence(c, opts)
+ // successful revert leaves current == via
+ c.Check(snapst.Current.N, Equals, opts.via)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 0)
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testRevertFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = true
+ opts.after = opts.before
+ s.fakeBackend.linkSnapFailTrigger = fmt.Sprintf("/snap/some-snap/%d", opts.via)
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed revert will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 0)
+ c.Check(s.fakeBackend.ops.Count("undo-copy-snap-data"), Equals, 0)
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testTotalRevertFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = true
+ opts.fail = true
+ opts.after = opts.before
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed revert will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 0)
+ c.Check(s.fakeBackend.ops.Count("undo-copy-snap-data"), Equals, 0)
+
+ return ts
+}
+
+// *** sequence tests ***
+
+// 1. a boring update
+// 1a. ... that works
+func (s *snapmgrTestSuite) TestSeqNormal(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 4, after: []int{2, 3, 4}})
+}
+
+// 1b. that fails during link
+func (s *snapmgrTestSuite) TestSeqNormalFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 4})
+}
+
+// 1c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalNormalFailure(c *C) {
+ // total updates are failures after sequence trimming => we lose a rev
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 4, after: []int{2, 3}})
+}
+
+// 2. a boring revert
+// 2a. that works
+func (s *snapmgrTestSuite) TestSeqRevert(c *C) {
+ s.testRevertSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+}
+
+// 2b. that fails during link
+func (s *snapmgrTestSuite) TestSeqRevertFailure(c *C) {
+ s.testRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+}
+
+// 2c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalRevertFailure(c *C) {
+ s.testTotalRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+}
+
+// 3. a post-revert update
+// 3a. that works
+func (s *snapmgrTestSuite) TestSeqPostRevert(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 4, after: []int{1, 2, 4}})
+}
+
+// 3b. that fails during link
+func (s *snapmgrTestSuite) TestSeqPostRevertFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 4})
+}
+
+// 3c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalPostRevertFailure(c *C) {
+ // lose a rev here as well
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 4, after: []int{1, 2}})
+}
+
+// 3d. manually requesting the one reverted away from
+func (s *snapmgrTestSuite) TestSeqRefreshPostRevertSameRevno(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 3, after: []int{1, 2, 3}})
+}
+
+// 4. a post-revert revert
+// 4a. that works
+func (s *snapmgrTestSuite) TestSeqRevertPostRevert(c *C) {
+ s.testRevertSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 1})
+}
+
+// 4b. that fails during link
+func (s *snapmgrTestSuite) TestSeqRevertPostRevertFailure(c *C) {
+ s.testRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 1})
+}
+
+// 4c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalRevertPostRevertFailure(c *C) {
+ s.testTotalRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 1})
+}
+
+// 5. an update that missed a rev
+// 5a. that works
+func (s *snapmgrTestSuite) TestSeqMissedOne(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2}, current: 2, via: 4, after: []int{1, 2, 4}})
+}
+
+// 5b. that fails during link
+func (s *snapmgrTestSuite) TestSeqMissedOneFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2}, current: 2, via: 4})
+}
+
+// 5c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalMissedOneFailure(c *C) {
+ // we don't lose a rev here because len(Seq) < 3 going in
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2}, current: 2, via: 4, after: []int{1, 2}})
+}
+
+// 6. an update that updates to a revision we already have ("ABA update")
+// 6a. that works
+func (s *snapmgrTestSuite) TestSeqABA(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2, after: []int{1, 3, 2}})
+ c.Check(s.fakeBackend.ops[len(s.fakeBackend.ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(2),
+ })
+}
+
+// 6b. that fails during link
+func (s *snapmgrTestSuite) TestSeqABAFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+ c.Check(s.fakeBackend.ops.First("cleanup-trash"), IsNil)
+}
+
+// 6c that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalABAFailure(c *C) {
+ // we don't lose a rev here because ABA
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2, after: []int{1, 2, 3}})
+ // XXX: TODO: NOTE!! WARNING!! etc
+ //
+ // if this happens in real life, things will be weird. revno 2 will
+ // have data that has been copied from 3, instead of old 2's data,
+ // because the failure occurred *after* nuking the trash. This can
+ // happen when things are chained. Because of this, if it were to
+ // *actually* happen the correct end sequence would be [1, 3] and not
+ // [1, 2, 3]. IRL this scenario can happen if an update that works is
+ // chained to an update that fails. Detecting this case is rather hard,
+ // and the end result is not nice, and we want to move cleanup to a
+ // separate handler & status that will cope with this better (so trash
+ // gets nuked after all tasks succeeded).
+}
+
+func (s *snapmgrTestSuite) TestUpdateTasksWithOldCurrent(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ si1 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
+ si2 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)}
+ si3 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(3)}
+ si4 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(4)}
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: []*snap.SideInfo{si1, si2, si3, si4},
+ Current: snap.R(2),
+ SnapType: "app",
+ })
+
+ // run the update
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ verifyInstallUpdateTasks(c, unlinkBefore|cleanupAfter, 2, ts, s.state)
+
+ // and ensure that it will remove the revisions after "current"
+ // (si3, si4)
+ var snapsup snapstate.SnapSetup
+ tasks := ts.Tasks()
+
+ i := len(tasks) - 6
+ c.Check(tasks[i].Kind(), Equals, "clear-snap")
+ err = tasks[i].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Revision(), Equals, si3.Revision)
+
+ i = len(tasks) - 4
+ c.Check(tasks[i].Kind(), Equals, "clear-snap")
+ err = tasks[i].Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Revision(), Equals, si4.Revision)
+}
+
+func (s *snapmgrTestSuite) TestUpdateCanDoBackwards(c *C) {
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+ si11 := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(11),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si7, &si11},
+ Current: si11.Revision,
+ SnapType: "app",
+ })
+
+ chg := s.state.NewChange("refresh", "refresh a snap")
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(7), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "some-snap",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "copy-data",
+ name: "/snap/some-snap/7",
+ old: "/snap/some-snap/11",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "",
+ Revision: snap.R(7),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+}
+
+func (s *snapmgrTestSuite) TestSnapStateNoLocalRevision(c *C) {
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(-7),
+ }
+ si11 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(-11),
+ }
+ snapst := &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{&si7, &si11},
+ Current: si7.Revision,
+ }
+ c.Assert(snapst.LocalRevision(), Equals, snap.R(-11))
+}
+
+func (s *snapmgrTestSuite) TestSnapStateLocalRevision(c *C) {
+ si7 := snap.SideInfo{
+ RealName: "some-snap",
+ Revision: snap.R(7),
+ }
+ snapst := &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{&si7},
+ Current: si7.Revision,
+ }
+ c.Assert(snapst.LocalRevision().Unset(), Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestInstallMany(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ installed, tts, err := snapstate.InstallMany(s.state, []string{"one", "two"}, 0)
+ c.Assert(err, IsNil)
+ c.Assert(tts, HasLen, 2)
+ c.Check(installed, DeepEquals, []string{"one", "two"})
+
+ for _, ts := range tts {
+ verifyInstallUpdateTasks(c, 0, 0, ts, s.state)
+ }
+}
+
+func (s *snapmgrTestSuite) TestRemoveMany(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "one", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "one", SnapID: "one-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ })
+ snapstate.Set(s.state, "two", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "two", SnapID: "two-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ })
+
+ removed, tts, err := snapstate.RemoveMany(s.state, []string{"one", "two"})
+ c.Assert(err, IsNil)
+ c.Assert(tts, HasLen, 2)
+ c.Check(removed, DeepEquals, []string{"one", "two"})
+
+ c.Assert(s.state.TaskCount(), Equals, 8*2)
+ for _, ts := range tts {
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-snap",
+ "remove-profiles",
+ "clear-snap",
+ "discard-snap",
+ "clear-aliases",
+ "discard-conns",
+ })
+ }
+}
+
+func taskWithKind(ts *state.TaskSet, kind string) *state.Task {
+ for _, task := range ts.Tasks() {
+ if task.Kind() == kind {
+ return task
+ }
+ }
+ return nil
+}
+
+var gadgetYaml = `
+defaults:
+ some-snap-id:
+ key: value
+
+volumes:
+ volume-id:
+ bootloader: grub
+`
+
+func (s *snapmgrTestSuite) prepareGadget(c *C) {
+ gadgetSideInfo := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
+ gadgetInfo := snaptest.MockSnap(c, "name: the-gadget\nversion: 1.0", "", gadgetSideInfo)
+
+ err := ioutil.WriteFile(filepath.Join(gadgetInfo.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0600)
+ c.Assert(err, IsNil)
+
+ snapstate.Set(s.state, "the-gadget", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&gadgetInfo.SideInfo},
+ Current: snap.R(1),
+ SnapType: "gadget",
+ })
+}
+
+func (s *snapmgrTestSuite) TestGadgetDefaults(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.prepareGadget(c)
+
+ snapPath := makeTestSnap(c, "name: some-snap\nversion: 1.0")
+
+ ts, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}, snapPath, "edge", snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ var m map[string]interface{}
+ runHook := taskWithKind(ts, "run-hook")
+ c.Assert(runHook.Kind(), Equals, "run-hook")
+ err = runHook.Get("hook-context", &m)
+ c.Assert(err, IsNil)
+ c.Assert(m["patch"], DeepEquals, map[string]interface{}{"key": "value"})
+}
+
+func (s *snapmgrTestSuite) TestGadgetDefaultsInstalled(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.prepareGadget(c)
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}},
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ snapPath := makeTestSnap(c, "name: some-snap\nversion: 1.0")
+
+ ts, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(2)}, snapPath, "edge", snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ var m map[string]interface{}
+ runHook := taskWithKind(ts, "run-hook")
+ c.Assert(runHook.Kind(), Equals, "run-hook")
+ err = runHook.Get("hook-context", &m)
+ c.Assert(err, Equals, state.ErrNoState)
+}
+
+type canDisableSuite struct{}
+
+var _ = Suite(&canDisableSuite{})
+
+func (s *canDisableSuite) TestCanDisable(c *C) {
+ for _, tt := range []struct {
+ typ snap.Type
+ canDisable bool
+ }{
+ {snap.TypeApp, true},
+ {snap.TypeGadget, false},
+ {snap.TypeKernel, false},
+ {snap.TypeOS, false},
+ } {
+ info := &snap.Info{Type: tt.typ}
+ c.Check(snapstate.CanDisable(info), Equals, tt.canDisable)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package snapstate implements the manager and state aspects responsible for the installation and removal of snaps.
+package snapstate
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "sort"
+
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/i18n/dumb"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup) (*state.TaskSet, error) {
+ if snapsup.Flags.Classic && !release.OnClassic {
+ return nil, fmt.Errorf("classic confinement is only supported on classic systems")
+ }
+ if !snapst.HasCurrent() { // install?
+ // check that the snap command namespace doesn't conflict with an enabled alias
+ if err := checkSnapAliasConflict(st, snapsup.Name()); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := checkChangeConflict(st, snapsup.Name(), snapst); err != nil {
+ return nil, err
+ }
+
+ targetRevision := snapsup.Revision()
+ revisionStr := ""
+ if snapsup.SideInfo != nil {
+ revisionStr = fmt.Sprintf(" (%s)", targetRevision)
+ }
+
+ // check if we already have the revision locally (alters tasks)
+ revisionIsLocal := snapst.LastIndex(targetRevision) >= 0
+
+ var prepare, prev *state.Task
+ fromStore := false
+ // if we have a local revision here we go back to that
+ if snapsup.SnapPath != "" || revisionIsLocal {
+ prepare = st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q%s"), snapsup.SnapPath, revisionStr))
+ } else {
+ fromStore = true
+ prepare = st.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q%s from channel %q"), snapsup.Name(), revisionStr, snapsup.Channel))
+ }
+ prepare.Set("snap-setup", snapsup)
+
+ tasks := []*state.Task{prepare}
+ addTask := func(t *state.Task) {
+ t.Set("snap-setup-task", prepare.ID())
+ t.WaitFor(prev)
+ tasks = append(tasks, t)
+ }
+ prev = prepare
+
+ if fromStore {
+ // fetch and check assertions
+ checkAsserts := st.NewTask("validate-snap", fmt.Sprintf(i18n.G("Fetch and check assertions for snap %q%s"), snapsup.Name(), revisionStr))
+ addTask(checkAsserts)
+ prev = checkAsserts
+ }
+
+ // mount
+ if !revisionIsLocal {
+ mount := st.NewTask("mount-snap", fmt.Sprintf(i18n.G("Mount snap %q%s"), snapsup.Name(), revisionStr))
+ addTask(mount)
+ prev = mount
+ }
+
+ if snapst.Active {
+ // unlink-current-snap (will stop services for copy-data)
+ stop := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q services"), snapsup.Name()))
+ addTask(stop)
+ prev = stop
+
+ removeAliases := st.NewTask("remove-aliases", fmt.Sprintf(i18n.G("Remove aliases for snap %q"), snapsup.Name()))
+ addTask(removeAliases)
+ prev = removeAliases
+
+ unlink := st.NewTask("unlink-current-snap", fmt.Sprintf(i18n.G("Make current revision for snap %q unavailable"), snapsup.Name()))
+ addTask(unlink)
+ prev = unlink
+ }
+
+ // copy-data (needs stopped services by unlink)
+ if !snapsup.Flags.Revert {
+ copyData := st.NewTask("copy-snap-data", fmt.Sprintf(i18n.G("Copy snap %q data"), snapsup.Name()))
+ addTask(copyData)
+ prev = copyData
+ }
+
+ // security
+ setupSecurity := st.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q%s security profiles"), snapsup.Name(), revisionStr))
+ addTask(setupSecurity)
+ prev = setupSecurity
+
+ // finalize (wrappers+current symlink)
+ linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q%s available to the system"), snapsup.Name(), revisionStr))
+ addTask(linkSnap)
+ prev = linkSnap
+
+ // setup aliases
+ setAutoAliases := st.NewTask("set-auto-aliases", fmt.Sprintf(i18n.G("Set automatic aliases for snap %q"), snapsup.Name()))
+ addTask(setAutoAliases)
+ prev = setAutoAliases
+
+ setupAliases := st.NewTask("setup-aliases", fmt.Sprintf(i18n.G("Setup snap %q aliases"), snapsup.Name()))
+ addTask(setupAliases)
+ prev = setupAliases
+
+ // run new serices
+ startSnapServices := st.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap %q%s services"), snapsup.Name(), revisionStr))
+ addTask(startSnapServices)
+ prev = startSnapServices
+
+ // Do not do that if we are reverting to a local revision
+ if snapst.HasCurrent() && !snapsup.Flags.Revert {
+ seq := snapst.Sequence
+ currentIndex := snapst.LastIndex(snapst.Current)
+
+ // discard everything after "current" (we may have reverted to
+ // a previous versions earlier)
+ for i := currentIndex + 1; i < len(seq); i++ {
+ si := seq[i]
+ if si.Revision == targetRevision {
+ // but don't discard this one; its' the thing we're switching to!
+ continue
+ }
+ ts := removeInactiveRevision(st, snapsup.Name(), si.Revision)
+ ts.WaitFor(prev)
+ tasks = append(tasks, ts.Tasks()...)
+ prev = tasks[len(tasks)-1]
+ }
+
+ // make sure we're not scheduling the removal of the target
+ // revision in the case where the target revision is already in
+ // the sequence.
+ for i := 0; i < currentIndex; i++ {
+ si := seq[i]
+ if si.Revision == targetRevision {
+ // we do *not* want to removeInactiveRevision of this one
+ copy(seq[i:], seq[i+1:])
+ seq = seq[:len(seq)-1]
+ currentIndex--
+ }
+ }
+
+ // normal garbage collect
+ for i := 0; i <= currentIndex-2; i++ {
+ si := seq[i]
+ if boot.InUse(snapsup.Name(), si.Revision) {
+ continue
+ }
+ ts := removeInactiveRevision(st, snapsup.Name(), si.Revision)
+ ts.WaitFor(prev)
+ tasks = append(tasks, ts.Tasks()...)
+ prev = tasks[len(tasks)-1]
+ }
+
+ addTask(st.NewTask("cleanup", fmt.Sprintf("Clean up %q%s install", snapsup.Name(), revisionStr)))
+ }
+
+ var defaults map[string]interface{}
+
+ if !snapst.HasCurrent() && snapsup.SideInfo != nil && snapsup.SideInfo.SnapID != "" {
+ gadget, err := GadgetInfo(st)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if err == nil {
+ gadgetInfo, err := snap.ReadGadgetInfo(gadget)
+ if err != nil {
+ return nil, err
+ }
+ defaults = gadgetInfo.Defaults[snapsup.SideInfo.SnapID]
+ }
+ }
+
+ installSet := state.NewTaskSet(tasks...)
+ configSet := Configure(st, snapsup.Name(), defaults)
+ configSet.WaitAll(installSet)
+ installSet.AddAll(configSet)
+
+ return installSet, nil
+}
+
+var Configure = func(st *state.State, snapName string, patch map[string]interface{}) *state.TaskSet {
+ panic("internal error: snapstate.Configure is unset")
+}
+
+func checkChangeConflict(st *state.State, snapName string, snapst *SnapState) error {
+ for _, task := range st.Tasks() {
+ k := task.Kind()
+ chg := task.Change()
+ if (k == "link-snap" || k == "unlink-snap" || k == "alias") && (chg == nil || !chg.Status().Ready()) {
+ snapsup, err := TaskSnapSetup(task)
+ if err != nil {
+ return fmt.Errorf("internal error: cannot obtain snap setup from task: %s", task.Summary())
+ }
+ if snapsup.Name() == snapName {
+ return fmt.Errorf("snap %q has changes in progress", snapName)
+ }
+ }
+ }
+
+ if snapst != nil {
+ // caller wants us to also make sure the SnapState in state
+ // matches the one they provided. Necessary because we need to
+ // unlock while talking to the store, during which a change can
+ // sneak in (if it's before the taskset is created) (e.g. for
+ // install, while getting the snap info; for refresh, when
+ // getting what needs refreshing).
+ var cursnapst SnapState
+ if err := Get(st, snapName, &cursnapst); err != nil && err != state.ErrNoState {
+ return err
+ }
+
+ // TODO: implement the rather-boring-but-more-performant SnapState.Equals
+ if !reflect.DeepEqual(snapst, &cursnapst) {
+ return fmt.Errorf("snap %q state changed during install preparations", snapName)
+ }
+ }
+
+ return nil
+}
+
+// InstallPath returns a set of tasks for installing snap from a file path.
+// Note that the state must be locked by the caller.
+// The provided SideInfo can contain just a name which results in a
+// local revision and sideloading, or full metadata in which case it
+// the snap will appear as installed from the store.
+func InstallPath(st *state.State, si *snap.SideInfo, path, channel string, flags Flags) (*state.TaskSet, error) {
+ name := si.RealName
+ if name == "" {
+ return nil, fmt.Errorf("internal error: snap name to install %q not provided", path)
+ }
+
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+
+ if si.SnapID != "" {
+ if si.Revision.Unset() {
+ return nil, fmt.Errorf("internal error: snap id set to install %q but revision is unset", path)
+ }
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: si,
+ SnapPath: path,
+ Channel: channel,
+ Flags: flags.ForSnapSetup(),
+ }
+
+ return doInstall(st, &snapst, snapsup)
+}
+
+// TryPath returns a set of tasks for trying a snap from a file path.
+// Note that the state must be locked by the caller.
+func TryPath(st *state.State, name, path string, flags Flags) (*state.TaskSet, error) {
+ flags.TryMode = true
+
+ return InstallPath(st, &snap.SideInfo{RealName: name}, path, "", flags)
+}
+
+// Install returns a set of tasks for installing snap.
+// Note that the state must be locked by the caller.
+func Install(st *state.State, name, channel string, revision snap.Revision, userID int, flags Flags) (*state.TaskSet, error) {
+ if channel == "" {
+ channel = "stable"
+ }
+
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if snapst.HasCurrent() {
+ return nil, &snap.AlreadyInstalledError{Snap: name}
+ }
+
+ info, err := snapInfo(st, name, channel, revision, userID)
+ if err != nil {
+ return nil, err
+ }
+
+ if !validInfoForFlags(info, &snapst, flags) {
+ return nil, store.ErrSnapNotFound
+ }
+
+ snapsup := &SnapSetup{
+ Channel: channel,
+ UserID: userID,
+ Flags: flags.ForSnapSetup(),
+ DownloadInfo: &info.DownloadInfo,
+ SideInfo: &info.SideInfo,
+ }
+
+ return doInstall(st, &snapst, snapsup)
+}
+
+// InstallMany installs everything from the given list of names.
+// Note that the state must be locked by the caller.
+func InstallMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ installed := make([]string, 0, len(names))
+ tasksets := make([]*state.TaskSet, 0, len(names))
+ for _, name := range names {
+ ts, err := Install(st, name, "", snap.R(0), userID, Flags{})
+ // FIXME: is this expected behavior?
+ if _, ok := err.(*snap.AlreadyInstalledError); ok {
+ continue
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ installed = append(installed, name)
+ tasksets = append(tasksets, ts)
+ }
+
+ return installed, tasksets, nil
+}
+
+// contains determines whether the given string is contained in the
+// given list of strings, which must have been previously sorted using
+// sort.Strings.
+func contains(ns []string, n string) bool {
+ i := sort.SearchStrings(ns, n)
+ if i >= len(ns) {
+ return false
+ }
+ return ns[i] == n
+}
+
+// RefreshCandidates gets a list of candidates for update
+// Note that the state must be locked by the caller.
+func RefreshCandidates(st *state.State, user *auth.UserState) ([]*snap.Info, error) {
+ updates, _, err := refreshCandidates(st, nil, user)
+ return updates, err
+}
+
+func refreshCandidates(st *state.State, names []string, user *auth.UserState) ([]*snap.Info, map[string]*SnapState, error) {
+ snapStates, err := All(st)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sort.Strings(names)
+
+ stateByID := make(map[string]*SnapState, len(snapStates))
+ candidatesInfo := make([]*store.RefreshCandidate, 0, len(snapStates))
+ for _, snapst := range snapStates {
+ if len(names) == 0 && (snapst.TryMode || snapst.DevMode) {
+ // no auto-refresh for trymode nor devmode
+ continue
+ }
+
+ // FIXME: snaps that are not active are skipped for now
+ // until we know what we want to do
+ if !snapst.Active {
+ continue
+ }
+
+ snapInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ // log something maybe?
+ continue
+ }
+
+ if snapInfo.SnapID == "" {
+ // no refresh for sideloaded
+ continue
+ }
+
+ if len(names) > 0 && !contains(names, snapInfo.Name()) {
+ continue
+ }
+
+ stateByID[snapInfo.SnapID] = snapst
+
+ // get confinement preference from the snapstate
+ candidateInfo := &store.RefreshCandidate{
+ // the desired channel (not info.Channel!)
+ Channel: snapst.Channel,
+ SnapID: snapInfo.SnapID,
+ Revision: snapInfo.Revision,
+ Epoch: snapInfo.Epoch,
+ }
+
+ if len(names) == 0 {
+ candidateInfo.Block = snapst.Block()
+ }
+
+ candidatesInfo = append(candidatesInfo, candidateInfo)
+ }
+
+ theStore := Store(st)
+
+ st.Unlock()
+ updates, err := theStore.ListRefresh(candidatesInfo, user)
+ st.Lock()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return updates, stateByID, nil
+}
+
+// ValidateRefreshes allows to hook validation into the handling of refresh candidates.
+var ValidateRefreshes func(st *state.State, refreshes []*snap.Info, userID int) (validated []*snap.Info, err error)
+
+// UpdateMany updates everything from the given list of names that the
+// store says is updateable. If the list is empty, update everything.
+// Note that the state must be locked by the caller.
+func UpdateMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
+ user, err := userFromUserID(st, userID)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ updates, stateByID, err := refreshCandidates(st, names, user)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if ValidateRefreshes != nil && len(updates) != 0 {
+ updates, err = ValidateRefreshes(st, updates, userID)
+ if err != nil {
+ // not doing "refresh all" report the error
+ if len(names) != 0 {
+ return nil, nil, err
+ }
+ // doing "refresh all", log the problems
+ logger.Noticef("cannot refresh some snaps: %v", err)
+ }
+ }
+
+ params := func(update *snap.Info) (string, Flags, *SnapState) {
+ snapst := stateByID[update.SnapID]
+ return snapst.Channel, snapst.Flags, snapst
+
+ }
+
+ return doUpdate(st, names, updates, params, userID)
+}
+
+func doUpdate(st *state.State, names []string, updates []*snap.Info, params func(*snap.Info) (channel string, flags Flags, snapst *SnapState), userID int) ([]string, []*state.TaskSet, error) {
+ tasksets := make([]*state.TaskSet, 0, len(updates))
+
+ refreshAll := len(names) == 0
+ var nameSet map[string]bool
+ if len(names) != 0 {
+ nameSet = make(map[string]bool, len(names))
+ for _, name := range names {
+ nameSet[name] = true
+ }
+ }
+
+ newAutoAliases, mustRetireAutoAliases, transferTargets, err := autoAliasesUpdate(st, names, updates)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ reportUpdated := make(map[string]bool, len(updates))
+ var retiredAutoAliasesTs *state.TaskSet
+
+ if len(mustRetireAutoAliases) != 0 {
+ var err error
+ retiredAutoAliasesTs, err = applyAutoAliasesDelta(st, mustRetireAutoAliases, "retire", refreshAll, func(snapName string, _ *state.TaskSet) {
+ if nameSet[snapName] {
+ reportUpdated[snapName] = true
+ }
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+ tasksets = append(tasksets, retiredAutoAliasesTs)
+ }
+
+ // wait for the auto-alias retire tasks as needed
+ scheduleUpdate := func(snapName string, ts *state.TaskSet) {
+ if retiredAutoAliasesTs != nil && (mustRetireAutoAliases[snapName] != nil || transferTargets[snapName]) {
+ ts.WaitAll(retiredAutoAliasesTs)
+ }
+ reportUpdated[snapName] = true
+ }
+
+ for _, update := range updates {
+ channel, flags, snapst := params(update)
+
+ if !validInfoForFlags(update, snapst, flags) {
+ // XXX: log something
+ continue
+ }
+
+ snapsup := &SnapSetup{
+ Channel: channel,
+ UserID: userID,
+ Flags: flags.ForSnapSetup(),
+ DownloadInfo: &update.DownloadInfo,
+ SideInfo: &update.SideInfo,
+ }
+
+ ts, err := doInstall(st, snapst, snapsup)
+ if err != nil {
+ if refreshAll {
+ // doing "refresh all", just skip this snap
+ logger.Noticef("cannot refresh snap %q: %v", update.Name(), err)
+ continue
+ }
+ return nil, nil, err
+ }
+ ts.JoinLane(st.NewLane())
+
+ scheduleUpdate(update.Name(), ts)
+ tasksets = append(tasksets, ts)
+ }
+
+ if len(newAutoAliases) != 0 {
+ addAutoAliasesTs, err := applyAutoAliasesDelta(st, newAutoAliases, "enable", refreshAll, scheduleUpdate)
+ if err != nil {
+ return nil, nil, err
+ }
+ tasksets = append(tasksets, addAutoAliasesTs)
+ }
+
+ updated := make([]string, 0, len(reportUpdated))
+ for name := range reportUpdated {
+ updated = append(updated, name)
+ }
+
+ return updated, tasksets, nil
+}
+
+func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string, refreshAll bool, linkTs func(snapName string, ts *state.TaskSet)) (*state.TaskSet, error) {
+ applyTs := state.NewTaskSet()
+ for snapName, aliases := range delta {
+ ts, err := ResetAliases(st, snapName, aliases)
+ if err != nil {
+ if refreshAll {
+ // doing "refresh all", just skip this snap
+ logger.Noticef("cannot %s automatic aliases for snap %q: %v", op, snapName, err)
+ continue
+ }
+ return nil, err
+ }
+ linkTs(snapName, ts)
+ applyTs.AddAll(ts)
+ }
+ return applyTs, nil
+}
+
+func autoAliasesUpdate(st *state.State, names []string, updates []*snap.Info) (new map[string][]string, mustRetire map[string][]string, transferTargets map[string]bool, err error) {
+ new, retired, err := AutoAliasesDelta(st, nil)
+ if err != nil {
+ if len(names) != 0 {
+ // not "refresh all", error
+ return nil, nil, nil, err
+ }
+ // log and continue
+ logger.Noticef("cannot find the delta for automatic aliases for some snaps: %v", err)
+ }
+
+ refreshAll := len(names) == 0
+
+ // retired alias -> snapName
+ retiringAliases := make(map[string]string, len(retired))
+ for snapName, aliases := range retired {
+ for _, alias := range aliases {
+ retiringAliases[alias] = snapName
+ }
+ }
+
+ // filter new considering only names if set:
+ // we add auto-aliases only for mentioned snaps
+ if !refreshAll && len(new) != 0 {
+ filteredNew := make(map[string][]string, len(new))
+ for _, name := range names {
+ if new[name] != nil {
+ filteredNew[name] = new[name]
+ }
+ }
+ new = filteredNew
+ }
+
+ // mark snaps that are sources or target of transfers
+ transferSources := make(map[string]bool, len(retired))
+ transferTargets = make(map[string]bool, len(new))
+ for snapName, aliases := range new {
+ for _, alias := range aliases {
+ if source := retiringAliases[alias]; source != "" {
+ transferTargets[snapName] = true
+ transferSources[source] = true
+ }
+ }
+ }
+
+ // snaps with updates
+ updating := make(map[string]bool, len(updates))
+ for _, info := range updates {
+ updating[info.Name()] = true
+ }
+
+ // add explicitly auto-aliases only for snaps that are not updated
+ for snapName := range new {
+ if updating[snapName] {
+ delete(new, snapName)
+ }
+ }
+
+ // retire explicitly auto-aliases only for snaps that are mentioned
+ // and not updated OR the source of transfers
+ mustRetire = make(map[string][]string, len(retired))
+ for snapName := range transferSources {
+ mustRetire[snapName] = retired[snapName]
+ }
+ if refreshAll {
+ for snapName, aliases := range retired {
+ if !updating[snapName] {
+ mustRetire[snapName] = aliases
+ }
+ }
+ } else {
+ for _, name := range names {
+ if !updating[name] && retired[name] != nil {
+ mustRetire[name] = retired[name]
+ }
+ }
+ }
+
+ return new, mustRetire, transferTargets, nil
+}
+
+// Update initiates a change updating a snap.
+// Note that the state must be locked by the caller.
+func Update(st *state.State, name, channel string, revision snap.Revision, userID int, flags Flags) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if !snapst.HasCurrent() {
+ return nil, fmt.Errorf("cannot find snap %q", name)
+ }
+
+ // FIXME: snaps that are not active are skipped for now
+ // until we know what we want to do
+ if !snapst.Active {
+ return nil, fmt.Errorf("refreshing disabled snap %q not supported", name)
+ }
+
+ if channel == "" {
+ channel = snapst.Channel
+ }
+
+ if !(flags.JailMode || flags.DevMode) {
+ flags.Classic = flags.Classic || snapst.Flags.Classic
+ }
+
+ var updates []*snap.Info
+ info, infoErr := infoForUpdate(st, &snapst, name, channel, revision, userID, flags)
+ if infoErr != nil {
+ if _, ok := infoErr.(*snap.NoUpdateAvailableError); !ok {
+ return nil, infoErr
+ }
+ // there may be some new auto-aliases
+ } else {
+ updates = append(updates, info)
+ }
+
+ params := func(update *snap.Info) (string, Flags, *SnapState) {
+ return channel, flags, &snapst
+ }
+
+ _, tts, err := doUpdate(st, []string{name}, updates, params, userID)
+ if err != nil {
+ return nil, err
+ }
+ if len(tts) == 0 && len(updates) == 0 {
+ // really nothing to do, return the original no-update-available error
+ return nil, infoErr
+ }
+ flat := state.NewTaskSet()
+ for _, ts := range tts {
+ flat.AddAll(ts)
+ }
+ return flat, nil
+}
+
+func validInfoForFlags(info *snap.Info, snapst *SnapState, flags Flags) bool {
+ switch c := info.Confinement; c {
+ case snap.StrictConfinement, "":
+ // strict is always fine
+ return true
+ case snap.DevModeConfinement:
+ // --devmode needs to be specified every time (==> ignore snapst)
+ return flags.DevModeAllowed()
+ case snap.ClassicConfinement:
+ if flags.Classic {
+ return true
+ }
+
+ if snapst != nil && snapst.Flags.Classic {
+ return true
+ }
+
+ return false
+ default:
+ logger.Noticef("unknown confinement %q", c)
+ }
+
+ return false
+}
+
+func infoForUpdate(st *state.State, snapst *SnapState, name, channel string, revision snap.Revision, userID int, flags Flags) (*snap.Info, error) {
+ if revision.Unset() {
+ // good ol' refresh
+ info, err := updateInfo(st, snapst, channel, userID)
+ if err != nil {
+ return nil, err
+ }
+ if !validInfoForFlags(info, snapst, flags) {
+ return nil, snap.NoUpdateAvailableError{name}
+ }
+ if ValidateRefreshes != nil && !flags.IgnoreValidation {
+ _, err := ValidateRefreshes(st, []*snap.Info{info}, userID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return info, nil
+ }
+ var sideInfo *snap.SideInfo
+ for _, si := range snapst.Sequence {
+ if si.Revision == revision {
+ sideInfo = si
+ break
+ }
+ }
+ if sideInfo == nil {
+ // refresh from given revision from store
+ return snapInfo(st, name, channel, revision, userID)
+ }
+
+ // refresh-to-local
+ return readInfo(name, sideInfo)
+}
+
+// Enable sets a snap to the active state
+func Enable(st *state.State, name string) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err == state.ErrNoState {
+ return nil, fmt.Errorf("cannot find snap %q", name)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if snapst.Active {
+ return nil, fmt.Errorf("snap %q already enabled", name)
+ }
+
+ if err := checkChangeConflict(st, name, nil); err != nil {
+ return nil, err
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: snapst.CurrentSideInfo(),
+ }
+
+ prepareSnap := st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q (%s)"), snapsup.Name(), snapst.Current))
+ prepareSnap.Set("snap-setup", &snapsup)
+
+ linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) available to the system"), snapsup.Name(), snapst.Current))
+ linkSnap.Set("snap-setup", &snapsup)
+ linkSnap.WaitFor(prepareSnap)
+
+ // setup aliases
+ setupAliases := st.NewTask("setup-aliases", fmt.Sprintf(i18n.G("Setup snap %q aliases"), snapsup.Name()))
+ setupAliases.Set("snap-setup", &snapsup)
+ setupAliases.WaitFor(linkSnap)
+
+ startSnapServices := st.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap %q (%s) services"), snapsup.Name(), snapst.Current))
+ startSnapServices.Set("snap-setup", &snapsup)
+ startSnapServices.WaitFor(setupAliases)
+
+ return state.NewTaskSet(prepareSnap, linkSnap, setupAliases, startSnapServices), nil
+}
+
+// Disable sets a snap to the inactive state
+func Disable(st *state.State, name string) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err == state.ErrNoState {
+ return nil, fmt.Errorf("cannot find snap %q", name)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !snapst.Active {
+ return nil, fmt.Errorf("snap %q already disabled", name)
+ }
+
+ info, err := Info(st, name, snapst.Current)
+ if err != nil {
+ return nil, err
+ }
+ if !canDisable(info) {
+ return nil, fmt.Errorf("snap %q cannot be disabled", name)
+ }
+
+ if err := checkChangeConflict(st, name, nil); err != nil {
+ return nil, err
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: name,
+ Revision: snapst.Current,
+ },
+ }
+
+ stopSnapServices := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q (%s) services"), snapsup.Name(), snapst.Current))
+ stopSnapServices.Set("snap-setup", &snapsup)
+
+ removeAliases := st.NewTask("remove-aliases", fmt.Sprintf(i18n.G("Remove aliases for snap %q"), snapsup.Name()))
+ removeAliases.Set("snap-setup-task", stopSnapServices.ID())
+ removeAliases.WaitFor(stopSnapServices)
+
+ unlinkSnap := st.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) unavailable to the system"), snapsup.Name(), snapst.Current))
+ unlinkSnap.Set("snap-setup-task", stopSnapServices.ID())
+ unlinkSnap.WaitFor(removeAliases)
+
+ return state.NewTaskSet(stopSnapServices, removeAliases, unlinkSnap), nil
+}
+
+// canDisable verifies that a snap can be deactivated.
+func canDisable(st *snap.Info) bool {
+ for _, importantSnapType := range []snap.Type{snap.TypeGadget, snap.TypeKernel, snap.TypeOS} {
+ if importantSnapType == st.Type {
+ return false
+ }
+ }
+
+ return true
+}
+
+// canRemove verifies that a snap can be removed.
+func canRemove(si *snap.Info, active bool) bool {
+ // Gadget snaps should not be removed as they are a key
+ // building block for Gadgets. Pruning non active ones
+ // is acceptable.
+ if si.Type == snap.TypeGadget && active {
+ return false
+ }
+
+ // You never want to remove an active kernel or OS
+ if (si.Type == snap.TypeKernel || si.Type == snap.TypeOS) && active {
+ return false
+ }
+ // TODO: on classic likely let remove core even if active if it's only snap left.
+
+ // never remove anything that is used for booting
+ if boot.InUse(si.Name(), si.Revision) {
+ return false
+ }
+
+ return true
+}
+
+// Remove returns a set of tasks for removing snap.
+// Note that the state must be locked by the caller.
+func Remove(st *state.State, name string, revision snap.Revision) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+
+ if !snapst.HasCurrent() {
+ return nil, &snap.NotInstalledError{Snap: name, Rev: snap.R(0)}
+ }
+
+ if err := checkChangeConflict(st, name, nil); err != nil {
+ return nil, err
+ }
+
+ active := snapst.Active
+ var removeAll bool
+ if revision.Unset() {
+ removeAll = true
+ revision = snapst.Current
+ } else {
+ removeAll = false
+
+ if active {
+ if revision == snapst.Current {
+ msg := "cannot remove active revision %s of snap %q"
+ if len(snapst.Sequence) > 1 {
+ msg += " (revert first?)"
+ }
+ return nil, fmt.Errorf(msg, revision, name)
+ }
+ active = false
+ }
+
+ if !revisionInSequence(&snapst, revision) {
+ return nil, &snap.NotInstalledError{Snap: name, Rev: revision}
+ }
+ }
+
+ info, err := Info(st, name, revision)
+ if err != nil {
+ return nil, err
+ }
+
+ // check if this is something that can be removed
+ if !canRemove(info, active) {
+ return nil, fmt.Errorf("snap %q is not removable", name)
+ }
+
+ // main/current SnapSetup
+ snapsup := SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: name,
+ Revision: revision,
+ },
+ }
+
+ // trigger remove
+
+ full := state.NewTaskSet()
+ var chain *state.TaskSet
+
+ addNext := func(ts *state.TaskSet) {
+ if chain != nil {
+ ts.WaitAll(chain)
+ }
+ full.AddAll(ts)
+ chain = ts
+ }
+
+ if active { // unlink
+ stopSnapServices := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q services"), name))
+ stopSnapServices.Set("snap-setup", snapsup)
+
+ removeAliases := st.NewTask("remove-aliases", fmt.Sprintf(i18n.G("Remove aliases for snap %q"), name))
+ removeAliases.WaitFor(stopSnapServices)
+ removeAliases.Set("snap-setup-task", stopSnapServices.ID())
+
+ unlink := st.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q unavailable to the system"), name))
+ unlink.Set("snap-setup-task", stopSnapServices.ID())
+ unlink.WaitFor(removeAliases)
+
+ removeSecurity := st.NewTask("remove-profiles", fmt.Sprintf(i18n.G("Remove security profile for snap %q (%s)"), name, revision))
+ removeSecurity.WaitFor(unlink)
+ removeSecurity.Set("snap-setup-task", stopSnapServices.ID())
+
+ addNext(state.NewTaskSet(stopSnapServices, removeAliases, unlink, removeSecurity))
+ }
+
+ if removeAll || len(snapst.Sequence) == 1 {
+ seq := snapst.Sequence
+ for i := len(seq) - 1; i >= 0; i-- {
+ si := seq[i]
+ addNext(removeInactiveRevision(st, name, si.Revision))
+ }
+
+ clearAliases := st.NewTask("clear-aliases", fmt.Sprintf(i18n.G("Clear alias state for snap %q"), name))
+ clearAliases.Set("snap-setup", &SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: name,
+ },
+ })
+ discardConns := st.NewTask("discard-conns", fmt.Sprintf(i18n.G("Discard interface connections for snap %q (%s)"), name, revision))
+ discardConns.WaitFor(clearAliases)
+ discardConns.Set("snap-setup", &SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: name,
+ },
+ })
+ addNext(state.NewTaskSet(clearAliases, discardConns))
+
+ } else {
+ addNext(removeInactiveRevision(st, name, revision))
+ }
+
+ return full, nil
+}
+
+func removeInactiveRevision(st *state.State, name string, revision snap.Revision) *state.TaskSet {
+ snapsup := SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: name,
+ Revision: revision,
+ },
+ }
+
+ clearData := st.NewTask("clear-snap", fmt.Sprintf(i18n.G("Remove data for snap %q (%s)"), name, revision))
+ clearData.Set("snap-setup", snapsup)
+
+ discardSnap := st.NewTask("discard-snap", fmt.Sprintf(i18n.G("Remove snap %q (%s) from the system"), name, revision))
+ discardSnap.WaitFor(clearData)
+ discardSnap.Set("snap-setup-task", clearData.ID())
+
+ return state.NewTaskSet(clearData, discardSnap)
+}
+
+// RemoveMany removes everything from the given list of names.
+// Note that the state must be locked by the caller.
+func RemoveMany(st *state.State, names []string) ([]string, []*state.TaskSet, error) {
+ removed := make([]string, 0, len(names))
+ tasksets := make([]*state.TaskSet, 0, len(names))
+ for _, name := range names {
+ ts, err := Remove(st, name, snap.R(0))
+ // FIXME: is this expected behavior?
+ if _, ok := err.(*snap.NotInstalledError); ok {
+ continue
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ removed = append(removed, name)
+ tasksets = append(tasksets, ts)
+ }
+
+ return removed, tasksets, nil
+}
+
+// Revert returns a set of tasks for reverting to the previous version of the snap.
+// Note that the state must be locked by the caller.
+func Revert(st *state.State, name string, flags Flags) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+
+ pi := snapst.previousSideInfo()
+ if pi == nil {
+ return nil, fmt.Errorf("no revision to revert to")
+ }
+
+ return RevertToRevision(st, name, pi.Revision, flags)
+}
+
+func RevertToRevision(st *state.State, name string, rev snap.Revision, flags Flags) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+
+ if snapst.Current == rev {
+ return nil, fmt.Errorf("already on requested revision")
+ }
+
+ if !snapst.Active {
+ return nil, fmt.Errorf("cannot revert inactive snaps")
+ }
+ i := snapst.LastIndex(rev)
+ if i < 0 {
+ return nil, fmt.Errorf("cannot find revision %s for snap %q", rev, name)
+ }
+ flags.Revert = true
+ snapsup := &SnapSetup{
+ SideInfo: snapst.Sequence[i],
+ Flags: flags.ForSnapSetup(),
+ }
+ return doInstall(st, &snapst, snapsup)
+}
+
+// State/info accessors
+
+// Info returns the information about the snap with given name and revision.
+// Works also for a mounted candidate snap in the process of being installed.
+func Info(st *state.State, name string, revision snap.Revision) (*snap.Info, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err == state.ErrNoState {
+ return nil, fmt.Errorf("cannot find snap %q", name)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ for i := len(snapst.Sequence) - 1; i >= 0; i-- {
+ if si := snapst.Sequence[i]; si.Revision == revision {
+ return readInfo(name, si)
+ }
+ }
+
+ return nil, fmt.Errorf("cannot find snap %q at revision %s", name, revision.String())
+}
+
+// CurrentInfo returns the information about the current revision of a snap with the given name.
+func CurrentInfo(st *state.State, name string) (*snap.Info, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ info, err := snapst.CurrentInfo()
+ if err == ErrNoCurrent {
+ return nil, fmt.Errorf("cannot find snap %q", name)
+ }
+ return info, err
+}
+
+// Get retrieves the SnapState of the given snap.
+func Get(st *state.State, name string, snapst *SnapState) error {
+ var snaps map[string]*json.RawMessage
+ err := st.Get("snaps", &snaps)
+ if err != nil {
+ return err
+ }
+ raw, ok := snaps[name]
+ if !ok {
+ return state.ErrNoState
+ }
+ err = json.Unmarshal([]byte(*raw), &snapst)
+ if err != nil {
+ return fmt.Errorf("cannot unmarshal snap state: %v", err)
+ }
+ return nil
+}
+
+// All retrieves return a map from name to SnapState for all current snaps in the system state.
+func All(st *state.State) (map[string]*SnapState, error) {
+ // XXX: result is a map because sideloaded snaps carry no name
+ // atm in their sideinfos
+ var stateMap map[string]*SnapState
+ if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ curStates := make(map[string]*SnapState, len(stateMap))
+ for snapName, snapst := range stateMap {
+ if snapst.HasCurrent() {
+ curStates[snapName] = snapst
+ }
+ }
+ return curStates, nil
+}
+
+// Set sets the SnapState of the given snap, overwriting any earlier state.
+func Set(st *state.State, name string, snapst *SnapState) {
+ var snaps map[string]*json.RawMessage
+ err := st.Get("snaps", &snaps)
+ if err != nil && err != state.ErrNoState {
+ panic("internal error: cannot unmarshal snaps state: " + err.Error())
+ }
+ if snaps == nil {
+ snaps = make(map[string]*json.RawMessage)
+ }
+ if snapst == nil || (len(snapst.Sequence) == 0) {
+ delete(snaps, name)
+ } else {
+ data, err := json.Marshal(snapst)
+ if err != nil {
+ panic("internal error: cannot marshal snap state: " + err.Error())
+ }
+ raw := json.RawMessage(data)
+ snaps[name] = &raw
+ }
+ st.Set("snaps", snaps)
+}
+
+// ActiveInfos returns information about all active snaps.
+func ActiveInfos(st *state.State) ([]*snap.Info, error) {
+ var stateMap map[string]*SnapState
+ var infos []*snap.Info
+ if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ for snapName, snapst := range stateMap {
+ if !snapst.Active {
+ continue
+ }
+ snapInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ logger.Noticef("cannot retrieve info for snap %q: %s", snapName, err)
+ continue
+ }
+ infos = append(infos, snapInfo)
+ }
+ return infos, nil
+}
+
+func infoForType(st *state.State, snapType snap.Type) (*snap.Info, error) {
+ var stateMap map[string]*SnapState
+ if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ for _, snapst := range stateMap {
+ if !snapst.HasCurrent() {
+ continue
+ }
+ typ, err := snapst.Type()
+ if err != nil {
+ return nil, err
+ }
+ if typ != snapType {
+ continue
+ }
+ return snapst.CurrentInfo()
+ }
+
+ return nil, state.ErrNoState
+}
+
+// GadgetInfo finds the current gadget snap's info.
+func GadgetInfo(st *state.State) (*snap.Info, error) {
+ return infoForType(st, snap.TypeGadget)
+}
+
+// CoreInfo finds the current OS snap's info.
+func CoreInfo(st *state.State) (*snap.Info, error) {
+ return infoForType(st, snap.TypeOS)
+}
+
+// KernelInfo finds the current kernel snap's info.
+func KernelInfo(st *state.State) (*snap.Info, error) {
+ return infoForType(st, snap.TypeKernel)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+)
+
+// Status is used for status values for changes and tasks.
+type Status int
+
+// Admitted status values for changes and tasks.
+const (
+ // DefaultStatus is the standard computed status for a change or task.
+ // For tasks it's always mapped to DoStatus, and for change its mapped
+ // to an aggregation of its tasks' statuses. See Change.Status for details.
+ DefaultStatus Status = 0
+
+ // HoldStatus means the task should not run, perhaps as a consequence of an error on another task.
+ HoldStatus Status = 1
+
+ // DoStatus means the change or task is ready to start.
+ DoStatus Status = 2
+
+ // DoingStatus means the change or task is running or an attempt was made to run it.
+ DoingStatus Status = 3
+
+ // DoneStatus means the change or task was accomplished successfully.
+ DoneStatus Status = 4
+
+ // AbortStatus means the task should stop doing its activities and then undo.
+ AbortStatus Status = 5
+
+ // UndoStatus means the change or task should be undone, probably due to an error elsewhere.
+ UndoStatus Status = 6
+
+ // UndoingStatus means the change or task is being undone or an attempt was made to undo it.
+ UndoingStatus Status = 7
+
+ // UndoneStatus means a task was first done and then undone after an error elsewhere.
+ // Changes go directly into the error status instead of being marked as undone.
+ UndoneStatus Status = 8
+
+ // ErrorStatus means the change or task has errored out while running or being undone.
+ ErrorStatus Status = 9
+
+ nStatuses = iota
+)
+
+// Ready returns whether a task or change with this status needs further
+// work or has completed its attempt to perform the current goal.
+func (s Status) Ready() bool {
+ switch s {
+ case DoneStatus, UndoneStatus, HoldStatus, ErrorStatus:
+ return true
+ }
+ return false
+}
+
+func (s Status) String() string {
+ switch s {
+ case DefaultStatus:
+ return "Default"
+ case DoStatus:
+ return "Do"
+ case DoingStatus:
+ return "Doing"
+ case DoneStatus:
+ return "Done"
+ case AbortStatus:
+ return "Abort"
+ case UndoStatus:
+ return "Undo"
+ case UndoingStatus:
+ return "Undoing"
+ case UndoneStatus:
+ return "Undone"
+ case HoldStatus:
+ return "Hold"
+ case ErrorStatus:
+ return "Error"
+ }
+ panic(fmt.Sprintf("internal error: unknown task status code: %d", s))
+}
+
+// Change represents a tracked modification to the system state.
+//
+// The Change provides both the justification for individual tasks
+// to be performed and the grouping of them.
+//
+// As an example, if an administrator requests an interface connection,
+// multiple hooks might be individually run to accomplish the task. The
+// Change summary would reflect the request for an interface connection,
+// while the individual Task values would track the running of
+// the hooks themselves.
+type Change struct {
+ state *State
+ id string
+ kind string
+ summary string
+ status Status
+ clean bool
+ data customData
+ taskIDs []string
+ lanes int
+ ready chan struct{}
+
+ spawnTime time.Time
+ readyTime time.Time
+}
+
+func newChange(state *State, id, kind, summary string) *Change {
+ return &Change{
+ state: state,
+ id: id,
+ kind: kind,
+ summary: summary,
+ data: make(customData),
+ ready: make(chan struct{}),
+
+ spawnTime: timeNow(),
+ }
+}
+
+type marshalledChange struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Status Status `json:"status"`
+ Clean bool `json:"clean,omitempty"`
+ Data map[string]*json.RawMessage `json:"data,omitempty"`
+ TaskIDs []string `json:"task-ids,omitempty"`
+ Lanes int `json:"lanes,omitempty"`
+
+ SpawnTime time.Time `json:"spawn-time"`
+ ReadyTime *time.Time `json:"ready-time,omitempty"`
+}
+
+// MarshalJSON makes Change a json.Marshaller
+func (c *Change) MarshalJSON() ([]byte, error) {
+ c.state.reading()
+ var readyTime *time.Time
+ if !c.readyTime.IsZero() {
+ readyTime = &c.readyTime
+ }
+ return json.Marshal(marshalledChange{
+ ID: c.id,
+ Kind: c.kind,
+ Summary: c.summary,
+ Status: c.status,
+ Clean: c.clean,
+ Data: c.data,
+ TaskIDs: c.taskIDs,
+ Lanes: c.lanes,
+
+ SpawnTime: c.spawnTime,
+ ReadyTime: readyTime,
+ })
+}
+
+// UnmarshalJSON makes Change a json.Unmarshaller
+func (c *Change) UnmarshalJSON(data []byte) error {
+ if c.state != nil {
+ c.state.writing()
+ }
+ var unmarshalled marshalledChange
+ err := json.Unmarshal(data, &unmarshalled)
+ if err != nil {
+ return err
+ }
+ c.id = unmarshalled.ID
+ c.kind = unmarshalled.Kind
+ c.summary = unmarshalled.Summary
+ c.status = unmarshalled.Status
+ c.clean = unmarshalled.Clean
+ custData := unmarshalled.Data
+ if custData == nil {
+ custData = make(customData)
+ }
+ c.data = custData
+ c.taskIDs = unmarshalled.TaskIDs
+ c.lanes = unmarshalled.Lanes
+ c.ready = make(chan struct{})
+ c.spawnTime = unmarshalled.SpawnTime
+ if unmarshalled.ReadyTime != nil {
+ c.readyTime = *unmarshalled.ReadyTime
+ }
+ return nil
+}
+
+// finishUnmarshal is called after the state and tasks are accessible.
+func (c *Change) finishUnmarshal() {
+ if c.Status().Ready() {
+ close(c.ready)
+ }
+}
+
+// ID returns the individual random key for the change.
+func (c *Change) ID() string {
+ return c.id
+}
+
+// Kind returns the nature of the change for managers to know how to handle it.
+func (c *Change) Kind() string {
+ return c.kind
+}
+
+// Summary returns a summary describing what the change is about.
+func (c *Change) Summary() string {
+ return c.summary
+}
+
+// Set associates value with key for future consulting by managers.
+// The provided value must properly marshal and unmarshal with encoding/json.
+func (c *Change) Set(key string, value interface{}) {
+ c.state.writing()
+ c.data.set(key, value)
+}
+
+// Get unmarshals the stored value associated with the provided key
+// into the value parameter.
+func (c *Change) Get(key string, value interface{}) error {
+ c.state.reading()
+ return c.data.get(key, value)
+}
+
+var statusOrder = []Status{
+ AbortStatus,
+ UndoingStatus,
+ UndoStatus,
+ DoingStatus,
+ DoStatus,
+ ErrorStatus,
+ UndoneStatus,
+ DoneStatus,
+ HoldStatus,
+}
+
+func init() {
+ if len(statusOrder) != nStatuses-1 {
+ panic("statusOrder has wrong number of elements")
+ }
+}
+
+// Status returns the current status of the change.
+// If the status was not explicitly set the result is derived from the status
+// of the individual tasks related to the change, according to the following
+// decision sequence:
+//
+// - With at least one task in DoStatus, return DoStatus
+// - With at least one task in ErrorStatus, return ErrorStatus
+// - Otherwise, return DoneStatus
+//
+func (c *Change) Status() Status {
+ c.state.reading()
+ if c.status == DefaultStatus {
+ if len(c.taskIDs) == 0 {
+ return HoldStatus
+ }
+ statusStats := make([]int, nStatuses)
+ for _, tid := range c.taskIDs {
+ statusStats[c.state.tasks[tid].Status()]++
+ }
+ for _, s := range statusOrder {
+ if statusStats[s] > 0 {
+ return s
+ }
+ }
+ panic(fmt.Sprintf("internal error: cannot process change status: %v", statusStats))
+ }
+ return c.status
+}
+
+// SetStatus sets the change status, overriding the default behavior (see Status method).
+func (c *Change) SetStatus(s Status) {
+ c.state.writing()
+ c.status = s
+ if s.Ready() {
+ c.markReady()
+ }
+}
+
+func (c *Change) markReady() {
+ select {
+ case <-c.ready:
+ default:
+ close(c.ready)
+ }
+ if c.readyTime.IsZero() {
+ c.readyTime = timeNow()
+ }
+}
+
+// Ready returns a channel that is closed the first time the change becomes ready.
+func (c *Change) Ready() <-chan struct{} {
+ return c.ready
+}
+
+// taskStatusChanged is called by tasks when their status is changed,
+// to give the opportunity for the change to close its ready channel.
+func (c *Change) taskStatusChanged(t *Task, old, new Status) {
+ if old.Ready() == new.Ready() {
+ return
+ }
+ for _, tid := range c.taskIDs {
+ task := c.state.tasks[tid]
+ if task != t && !task.status.Ready() {
+ return
+ }
+ }
+ // Here is the exact moment when a change goes from unready to ready,
+ // and from ready to unready. For now handle only the first of those.
+ // For the latter the channel might be replaced in the future.
+ if c.IsReady() && !c.Status().Ready() {
+ panic(fmt.Errorf("change %s unexpectedly became unready (%s)", c.ID(), c.Status()))
+ }
+ c.markReady()
+}
+
+// IsClean returns whether all tasks in the change have been cleaned. See SetClean.
+func (c *Change) IsClean() bool {
+ c.state.reading()
+ return c.clean
+}
+
+// IsReady returns whether the change is considered ready.
+//
+// The result is similar to calling Ready on the status returned by the Status
+// method, but this function is more efficient as it doesn't need to recompute
+// the aggregated state of tasks on every call.
+//
+// As an exception, IsReady returns false for a Change without any tasks that
+// never had its status explicitly set and was never unmarshalled out of the
+// persistent state, despite its initial status being Hold. This is how the
+// system represents changes right after they are created.
+func (c *Change) IsReady() bool {
+ select {
+ case <-c.ready:
+ return true
+ default:
+ }
+ return false
+}
+
+func (c *Change) taskCleanChanged() {
+ if !c.IsReady() {
+ panic("internal error: attempted to set a task clean while change not ready")
+ }
+ for _, tid := range c.taskIDs {
+ task := c.state.tasks[tid]
+ if !task.clean {
+ return
+ }
+ }
+ c.clean = true
+}
+
+// SpawnTime returns the time when the change was created.
+func (c *Change) SpawnTime() time.Time {
+ c.state.reading()
+ return c.spawnTime
+}
+
+// ReadyTime returns the time when the change became ready.
+func (c *Change) ReadyTime() time.Time {
+ c.state.reading()
+ return c.readyTime
+}
+
+// changeError holds a set of task errors.
+type changeError struct {
+ errors []taskError
+}
+
+type taskError struct {
+ task string
+ error string
+}
+
+func (e *changeError) Error() string {
+ var buf bytes.Buffer
+ buf.WriteString("cannot perform the following tasks:\n")
+ for _, te := range e.errors {
+ fmt.Fprintf(&buf, "- %s (%s)\n", te.task, te.error)
+ }
+ return strings.TrimSuffix(buf.String(), "\n")
+}
+
+func stripErrorMsg(msg string) (string, bool) {
+ i := strings.Index(msg, " ")
+ if i >= 0 && strings.HasPrefix(msg[i:], " ERROR ") {
+ return msg[i+len(" ERROR "):], true
+ }
+ return "", false
+}
+
+// Err returns an error value based on errors that were logged for tasks registered
+// in this change, or nil if the change is not in ErrorStatus.
+func (c *Change) Err() error {
+ c.state.reading()
+ if c.Status() != ErrorStatus {
+ return nil
+ }
+ var errors []taskError
+ for _, tid := range c.taskIDs {
+ task := c.state.tasks[tid]
+ if task.Status() != ErrorStatus {
+ continue
+ }
+ for _, msg := range task.Log() {
+ if s, ok := stripErrorMsg(msg); ok {
+ errors = append(errors, taskError{task.Summary(), s})
+ }
+ }
+ }
+ if len(errors) == 0 {
+ return fmt.Errorf("internal inconsistency: change %q in ErrorStatus with no task errors logged", c.Kind())
+ }
+ return &changeError{errors}
+}
+
+// State returns the system State
+func (c *Change) State() *State {
+ return c.state
+}
+
+// AddTask registers a task as required for the state change to
+// be accomplished.
+func (c *Change) AddTask(t *Task) {
+ c.state.writing()
+ if t.change != "" {
+ panic(fmt.Sprintf("internal error: cannot add one %q task to multiple changes", t.Kind()))
+ }
+ t.change = c.id
+ c.taskIDs = addOnce(c.taskIDs, t.ID())
+}
+
+// AddAll registers all tasks in the set as required for the state
+// change to be accomplished.
+func (c *Change) AddAll(ts *TaskSet) {
+ c.state.writing()
+ for _, t := range ts.tasks {
+ c.AddTask(t)
+ }
+}
+
+// Tasks returns all the tasks this state change depends on.
+func (c *Change) Tasks() []*Task {
+ c.state.reading()
+ return c.state.tasksIn(c.taskIDs)
+}
+
+// Abort flags the change for cancellation, whether in progress or not.
+// Cancellation will proceed at the next ensure pass.
+func (c *Change) Abort() {
+ c.state.writing()
+ tasks := make([]*Task, len(c.taskIDs))
+ for i, tid := range c.taskIDs {
+ tasks[i] = c.state.tasks[tid]
+ }
+ c.abortTasks(tasks, make(map[int]bool))
+}
+
+// AbortLanes aborts all tasks in the provided lanes and any tasks waiting on them,
+// except for tasks that are also in a healthy lane (not aborted, and not waiting
+// on aborted).
+func (c *Change) AbortLanes(lanes []int) {
+ c.state.writing()
+ c.abortLanes(lanes, make(map[int]bool))
+}
+
+func (c *Change) abortLanes(lanes []int, abortedLanes map[int]bool) {
+ var hasLive = make(map[int]bool)
+ var hasDead = make(map[int]bool)
+ var laneTasks []*Task
+NextChangeTask:
+ for _, tid := range c.taskIDs {
+ t := c.state.tasks[tid]
+
+ var live bool
+ switch t.Status() {
+ case DoStatus, DoingStatus, DoneStatus:
+ live = true
+ }
+
+ for _, tlane := range t.Lanes() {
+ for _, lane := range lanes {
+ if tlane == lane {
+ laneTasks = append(laneTasks, t)
+ continue NextChangeTask
+ }
+ }
+
+ // Track opinion about lanes not in the kill list.
+ // If the lane ends up being entirely live, we'll
+ // preserve this task alive too.
+ if live {
+ hasLive[tlane] = true
+ } else {
+ hasDead[tlane] = true
+ }
+ }
+ }
+
+ abortTasks := make([]*Task, 0, len(laneTasks))
+NextLaneTask:
+ for _, t := range laneTasks {
+ for _, tlane := range t.Lanes() {
+ if hasLive[tlane] && !hasDead[tlane] {
+ continue NextLaneTask
+ }
+ }
+ abortTasks = append(abortTasks, t)
+ }
+
+ for _, lane := range lanes {
+ abortedLanes[lane] = true
+ }
+ if len(abortTasks) > 0 {
+ c.abortTasks(abortTasks, abortedLanes)
+ }
+}
+
+func (c *Change) abortTasks(tasks []*Task, abortedLanes map[int]bool) {
+ var lanes []int
+ for i := 0; i < len(tasks); i++ {
+ t := tasks[i]
+ switch t.Status() {
+ case DoStatus:
+ // Still pending so don't even start.
+ t.SetStatus(HoldStatus)
+ case DoingStatus:
+ // In progress so stop and undo it.
+ t.SetStatus(AbortStatus)
+ case DoneStatus:
+ // Already done so undo it.
+ t.SetStatus(UndoStatus)
+ }
+
+ for _, lane := range t.Lanes() {
+ if !abortedLanes[lane] {
+ lanes = append(lanes, t.Lanes()...)
+ }
+ }
+
+ for _, halted := range t.HaltTasks() {
+ tasks = append(tasks, halted)
+ }
+ }
+ if len(lanes) > 0 {
+ c.abortLanes(lanes, abortedLanes)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state_test
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/overlord/state"
+
+ . "gopkg.in/check.v1"
+)
+
+type changeSuite struct{}
+
+var _ = Suite(&changeSuite{})
+
+func (cs *changeSuite) TestNewChange(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "summary...")
+ c.Check(chg.Kind(), Equals, "install")
+ c.Check(chg.Summary(), Equals, "summary...")
+}
+
+func (cs *changeSuite) TestReadyTime(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "summary...")
+
+ now := time.Now()
+
+ t := chg.SpawnTime()
+ c.Check(t.After(now.Add(-5*time.Second)), Equals, true)
+ c.Check(t.Before(now.Add(5*time.Second)), Equals, true)
+
+ c.Check(chg.ReadyTime().IsZero(), Equals, true)
+
+ chg.SetStatus(state.DoneStatus)
+
+ t = chg.ReadyTime()
+ c.Check(t.After(now.Add(-5*time.Second)), Equals, true)
+ c.Check(t.Before(now.Add(5*time.Second)), Equals, true)
+}
+
+func (cs *changeSuite) TestStatusString(c *C) {
+ for s := state.Status(0); s < state.ErrorStatus+1; s++ {
+ c.Assert(s.String(), Matches, ".+")
+ }
+}
+
+func (cs *changeSuite) TestGetSet(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ chg.Set("a", 1)
+
+ var v int
+ err := chg.Get("a", &v)
+ c.Assert(err, IsNil)
+ c.Check(v, Equals, 1)
+}
+
+// TODO Better testing of full change roundtripping via JSON.
+
+func (cs *changeSuite) TestNewTaskAddTaskAndTasks(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ t1 := st.NewTask("download", "1...")
+ chg.AddTask(t1)
+ t2 := st.NewTask("verify", "2...")
+ chg.AddTask(t2)
+
+ tasks := chg.Tasks()
+ c.Check(tasks, DeepEquals, []*state.Task{t1, t2})
+ c.Check(t1.Change(), Equals, chg)
+ c.Check(t2.Change(), Equals, chg)
+
+ chg2 := st.NewChange("install", "...")
+ c.Check(func() { chg2.AddTask(t1) }, PanicMatches, `internal error: cannot add one "download" task to multiple changes`)
+}
+
+func (cs *changeSuite) TestAddAll(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("verify", "2...")
+ chg.AddAll(state.NewTaskSet(t1, t2))
+
+ tasks := chg.Tasks()
+ c.Check(tasks, DeepEquals, []*state.Task{t1, t2})
+ c.Check(t1.Change(), Equals, chg)
+ c.Check(t2.Change(), Equals, chg)
+}
+
+func (cs *changeSuite) TestStatusExplicitlyDefined(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+ c.Assert(chg.Status(), Equals, state.HoldStatus)
+
+ t := st.NewTask("download", "...")
+ chg.AddTask(t)
+
+ t.SetStatus(state.DoingStatus)
+ c.Assert(chg.Status(), Equals, state.DoingStatus)
+ chg.SetStatus(state.ErrorStatus)
+ c.Assert(chg.Status(), Equals, state.ErrorStatus)
+}
+
+func (cs *changeSuite) TestStatusDerivedFromTasks(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ // Nothing to do with it if there are no tasks.
+ c.Assert(chg.Status(), Equals, state.HoldStatus)
+
+ tasks := make(map[state.Status]*state.Task)
+
+ for s := state.DefaultStatus + 1; s < state.ErrorStatus+1; s++ {
+ t := st.NewTask("download", s.String())
+ t.SetStatus(s)
+ chg.AddTask(t)
+ tasks[s] = t
+ }
+
+ order := []state.Status{
+ state.AbortStatus,
+ state.UndoingStatus,
+ state.UndoStatus,
+ state.DoingStatus,
+ state.DoStatus,
+ state.ErrorStatus,
+ state.UndoneStatus,
+ state.DoneStatus,
+ state.HoldStatus,
+ }
+
+ for _, s := range order {
+ // Set all tasks with previous statuses to s as well.
+ for _, s2 := range order {
+ if s == s2 {
+ break
+ }
+ tasks[s2].SetStatus(s)
+ }
+ c.Assert(chg.Status(), Equals, s)
+ }
+}
+
+func (cs *changeSuite) TestCloseReadyOnExplicitStatus(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ select {
+ case <-chg.Ready():
+ c.Fatalf("Change should not be ready")
+ default:
+ }
+ c.Assert(chg.IsReady(), Equals, false)
+
+ chg.SetStatus(state.ErrorStatus)
+
+ select {
+ case <-chg.Ready():
+ default:
+ c.Fatalf("Change should be ready")
+ }
+ c.Assert(chg.IsReady(), Equals, true)
+}
+
+func (cs *changeSuite) TestCloseReadyWhenTasksReady(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+ t1 := st.NewTask("download", "...")
+ t2 := st.NewTask("download", "...")
+ chg.AddTask(t1)
+ chg.AddTask(t2)
+
+ select {
+ case <-chg.Ready():
+ c.Fatalf("Change should not be ready")
+ default:
+ }
+ c.Assert(chg.IsReady(), Equals, false)
+
+ t1.SetStatus(state.DoneStatus)
+
+ select {
+ case <-chg.Ready():
+ c.Fatalf("Change should not be ready")
+ default:
+ }
+ c.Assert(chg.IsReady(), Equals, false)
+
+ t2.SetStatus(state.DoneStatus)
+
+ select {
+ case <-chg.Ready():
+ default:
+ c.Fatalf("Change should be ready")
+ }
+ c.Assert(chg.IsReady(), Equals, true)
+}
+
+func (cs *changeSuite) TestIsClean(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("verify", "2...")
+ chg.AddAll(state.NewTaskSet(t1, t2))
+
+ t1.SetStatus(state.DoneStatus)
+ c.Assert(t1.SetClean, PanicMatches, ".*while change not ready")
+ t2.SetStatus(state.DoneStatus)
+
+ t1.SetClean()
+ c.Assert(chg.IsClean(), Equals, false)
+ t2.SetClean()
+ c.Assert(chg.IsClean(), Equals, true)
+}
+
+func (cs *changeSuite) TestState(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ st.Unlock()
+
+ c.Assert(chg.State(), Equals, st)
+}
+
+func (cs *changeSuite) TestErr(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ t1 := st.NewTask("download", "Download")
+ t2 := st.NewTask("activate", "Activate")
+
+ chg.AddTask(t1)
+ chg.AddTask(t2)
+
+ c.Assert(chg.Err(), IsNil)
+
+ // t2 still running so change not yet in ErrorStatus
+ t1.SetStatus(state.ErrorStatus)
+ c.Assert(chg.Err(), IsNil)
+
+ t2.SetStatus(state.ErrorStatus)
+ c.Assert(chg.Err(), ErrorMatches, `internal inconsistency: change "install" in ErrorStatus with no task errors logged`)
+
+ t1.Errorf("Download error")
+ c.Assert(chg.Err(), ErrorMatches, ""+
+ "cannot perform the following tasks:\n"+
+ "- Download \\(Download error\\)")
+
+ t2.Errorf("Activate error")
+ c.Assert(chg.Err(), ErrorMatches, ""+
+ "cannot perform the following tasks:\n"+
+ "- Download \\(Download error\\)\n"+
+ "- Activate \\(Activate error\\)")
+}
+
+func (cs *changeSuite) TestMethodEntrance(c *C) {
+ st := state.New(&fakeStateBackend{})
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ st.Unlock()
+
+ writes := []func(){
+ func() { chg.Set("a", 1) },
+ func() { chg.SetStatus(state.DoStatus) },
+ func() { chg.AddTask(nil) },
+ func() { chg.AddAll(nil) },
+ func() { chg.UnmarshalJSON(nil) },
+ }
+
+ reads := []func(){
+ func() { chg.Get("a", nil) },
+ func() { chg.Status() },
+ func() { chg.IsClean() },
+ func() { chg.Tasks() },
+ func() { chg.Err() },
+ func() { chg.MarshalJSON() },
+ func() { chg.SpawnTime() },
+ func() { chg.ReadyTime() },
+ }
+
+ for i, f := range reads {
+ c.Logf("Testing read function #%d", i)
+ c.Assert(f, PanicMatches, "internal error: accessing state without lock")
+ c.Assert(st.Modified(), Equals, false)
+ }
+
+ for i, f := range writes {
+ st.Lock()
+ st.Unlock()
+ c.Assert(st.Modified(), Equals, false)
+
+ c.Logf("Testing write function #%d", i)
+ c.Assert(f, PanicMatches, "internal error: accessing state without lock")
+ c.Assert(st.Modified(), Equals, true)
+ }
+}
+
+func (cs *changeSuite) TestAbort(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ for s := state.DefaultStatus + 1; s < state.ErrorStatus+1; s++ {
+ t := st.NewTask("download", s.String())
+ t.SetStatus(s)
+ t.Set("old-status", s)
+ chg.AddTask(t)
+ }
+
+ chg.Abort()
+
+ tasks := chg.Tasks()
+ for _, t := range tasks {
+ var s state.Status
+ err := t.Get("old-status", &s)
+ c.Assert(err, IsNil)
+
+ c.Logf("Checking %s task after abort", t.Summary())
+ switch s {
+ case state.DoStatus:
+ c.Assert(t.Status(), Equals, state.HoldStatus)
+ case state.DoneStatus:
+ c.Assert(t.Status(), Equals, state.UndoStatus)
+ case state.DoingStatus:
+ c.Assert(t.Status(), Equals, state.AbortStatus)
+ default:
+ c.Assert(t.Status(), Equals, s)
+ }
+ }
+}
+
+// Task wait order:
+//
+// => t21 => t22
+// / \
+// t11 => t12 => t41 => t42
+// \ /
+// => t31 => t32
+//
+// setup and result lines are <task>:<status>[:<lane>,...]
+//
+// "*" as task name means "all remaining".
+//
+var abortLanesTests = []struct {
+ setup string
+ abort []int
+ result string
+}{
+
+ // Some basics.
+ {
+ setup: "*:do",
+ abort: []int{},
+ result: "*:do",
+ }, {
+ setup: "*:do",
+ abort: []int{1},
+ result: "*:do",
+ }, {
+ setup: "*:do",
+ abort: []int{0},
+ result: "*:hold",
+ }, {
+ setup: "t11:done t12:doing t22:do",
+ abort: []int{0},
+ result: "t11:undo t12:abort t22:hold",
+ },
+
+ // => t21 (2) => t22 (2)
+ // / \
+ // t11 (1) => t12 (1) => t41 (4) => t42 (4)
+ // \ /
+ // => t31 (3) => t32 (3)
+ {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{0},
+ result: "*:do",
+ }, {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{1},
+ result: "*:hold",
+ }, {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{2},
+ result: "t21:hold t22:hold t41:hold t42:hold *:do",
+ }, {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{3},
+ result: "t31:hold t32:hold t41:hold t42:hold *:do",
+ }, {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{2, 3},
+ result: "t21:hold t22:hold t31:hold t32:hold t41:hold t42:hold *:do",
+ }, {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{4},
+ result: "t41:hold t42:hold *:do",
+ }, {
+ setup: "t11:do:1 t12:do:1 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{5},
+ result: "*:do",
+ },
+
+ // => t21 (2) => t22 (2)
+ // / \
+ // t11 (2,3) => t12 (2,3) => t41 (4) => t42 (4)
+ // \ /
+ // => t31 (3) => t32 (3)
+ {
+ setup: "t11:do:2,3 t12:do:2,3 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{2},
+ result: "t21:hold t22:hold t41:hold t42:hold *:do",
+ }, {
+ setup: "t11:do:2,3 t12:do:2,3 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{3},
+ result: "t31:hold t32:hold t41:hold t42:hold *:do",
+ }, {
+ setup: "t11:do:2,3 t12:do:2,3 t21:do:2 t22:do:2 t31:do:3 t32:do:3 t41:do:4 t42:do:4",
+ abort: []int{2, 3},
+ result: "*:hold",
+ },
+
+ // => t21 (1) => t22 (1)
+ // / \
+ // t11 (1) => t12 (1) => t41 (4) => t42 (4)
+ // \ /
+ // => t31 (1) => t32 (1)
+ {
+ setup: "t41:error:4 t42:do:4 *:do:1",
+ abort: []int{1},
+ result: "t41:error *:hold",
+ },
+}
+
+func (ts *taskRunnerSuite) TestAbortLanes(c *C) {
+
+ names := strings.Fields("t11 t12 t21 t22 t31 t32 t41 t42")
+
+ for _, test := range abortLanesTests {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ st.Lock()
+ defer st.Unlock()
+
+ c.Assert(len(st.Tasks()), Equals, 0)
+
+ chg := st.NewChange("install", "...")
+ tasks := make(map[string]*state.Task)
+ for _, name := range names {
+ tasks[name] = st.NewTask("do", name)
+ chg.AddTask(tasks[name])
+ }
+ tasks["t12"].WaitFor(tasks["t11"])
+ tasks["t21"].WaitFor(tasks["t12"])
+ tasks["t22"].WaitFor(tasks["t21"])
+ tasks["t31"].WaitFor(tasks["t12"])
+ tasks["t32"].WaitFor(tasks["t31"])
+ tasks["t41"].WaitFor(tasks["t22"])
+ tasks["t41"].WaitFor(tasks["t32"])
+ tasks["t42"].WaitFor(tasks["t41"])
+
+ c.Logf("-----")
+ c.Logf("Testing setup: %s", test.setup)
+
+ statuses := make(map[string]state.Status)
+ for s := state.DefaultStatus; s <= state.ErrorStatus; s++ {
+ statuses[strings.ToLower(s.String())] = s
+ }
+
+ items := strings.Fields(test.setup)
+ seen := make(map[string]bool)
+ for i := 0; i < len(items); i++ {
+ item := items[i]
+ parts := strings.Split(item, ":")
+ if parts[0] == "*" {
+ for _, name := range names {
+ if !seen[name] {
+ parts[0] = name
+ items = append(items, strings.Join(parts, ":"))
+ }
+ }
+ continue
+ }
+ seen[parts[0]] = true
+ task := tasks[parts[0]]
+ task.SetStatus(statuses[parts[1]])
+ if len(parts) > 2 {
+ lanes := strings.Split(parts[2], ",")
+ for _, lane := range lanes {
+ n, err := strconv.Atoi(lane)
+ c.Assert(err, IsNil)
+ task.JoinLane(n)
+ }
+ }
+ }
+
+ c.Logf("Aborting with: %v", test.abort)
+
+ chg.AbortLanes(test.abort)
+
+ c.Logf("Expected result: %s", test.result)
+
+ seen = make(map[string]bool)
+ var expected = strings.Fields(test.result)
+ var obtained []string
+ for i := 0; i < len(expected); i++ {
+ item := expected[i]
+ parts := strings.Split(item, ":")
+ if parts[0] == "*" {
+ var expanded []string
+ for _, name := range names {
+ if !seen[name] {
+ parts[0] = name
+ expanded = append(expanded, strings.Join(parts, ":"))
+ }
+ }
+ expected = append(expected[:i], append(expanded, expected[i+1:]...)...)
+ i--
+ continue
+ }
+ name := parts[0]
+ seen[parts[0]] = true
+ obtained = append(obtained, name+":"+strings.ToLower(tasks[name].Status().String()))
+ }
+
+ c.Assert(strings.Join(obtained, " "), Equals, strings.Join(expected, " "), Commentf("setup: %s", test.setup))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state
+
+import (
+ "time"
+)
+
+// MockCheckpointRetryDelay changes unlockCheckpointRetryInterval and unlockCheckpointRetryMaxTime.
+func MockCheckpointRetryDelay(retryInterval, retryMaxTime time.Duration) (restore func()) {
+ oldInterval := unlockCheckpointRetryInterval
+ oldMaxTime := unlockCheckpointRetryMaxTime
+ unlockCheckpointRetryInterval = retryInterval
+ unlockCheckpointRetryMaxTime = retryMaxTime
+ return func() {
+ unlockCheckpointRetryInterval = oldInterval
+ unlockCheckpointRetryMaxTime = oldMaxTime
+ }
+}
+
+func MockChangeTimes(chg *Change, spawnTime, readyTime time.Time) {
+ chg.spawnTime = spawnTime
+ chg.readyTime = readyTime
+}
+
+func MockTaskTimes(t *Task, spawnTime, readyTime time.Time) {
+ t.spawnTime = spawnTime
+ t.readyTime = readyTime
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package state implements the representation of system state.
+package state
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/snapcore/snapd/logger"
+)
+
+// A Backend is used by State to checkpoint on every unlock operation
+// and to mediate requests to ensure the state sooner or request restarts.
+type Backend interface {
+ Checkpoint(data []byte) error
+ EnsureBefore(d time.Duration)
+ // TODO: take flags to ask for reboot vs restart?
+ RequestRestart(t RestartType)
+}
+
+type customData map[string]*json.RawMessage
+
+func (data customData) get(key string, value interface{}) error {
+ entryJSON := data[key]
+ if entryJSON == nil {
+ return ErrNoState
+ }
+ err := json.Unmarshal(*entryJSON, value)
+ if err != nil {
+ return fmt.Errorf("internal error: could not unmarshal state entry %q: %v", key, err)
+ }
+ return nil
+}
+
+func (data customData) set(key string, value interface{}) {
+ serialized, err := json.Marshal(value)
+ if err != nil {
+ logger.Panicf("internal error: could not marshal value for state entry %q: %v", key, err)
+ }
+ entryJSON := json.RawMessage(serialized)
+ data[key] = &entryJSON
+}
+
+type RestartType int
+
+const (
+ RestartUnset RestartType = iota
+ RestartDaemon
+ RestartSystem
+)
+
+// State represents an evolving system state that persists across restarts.
+//
+// The State is concurrency-safe, and all reads and writes to it must be
+// performed with the state locked. It's a runtime error (panic) to perform
+// operations without it.
+//
+// The state is persisted on every unlock operation via the StateBackend
+// it was initialized with.
+type State struct {
+ mu sync.Mutex
+ muC int32
+
+ lastTaskId int
+ lastChangeId int
+ lastLaneId int
+
+ backend Backend
+ data customData
+ changes map[string]*Change
+ tasks map[string]*Task
+
+ modified bool
+
+ cache map[interface{}]interface{}
+}
+
+// New returns a new empty state.
+func New(backend Backend) *State {
+ return &State{
+ backend: backend,
+ data: make(customData),
+ changes: make(map[string]*Change),
+ tasks: make(map[string]*Task),
+ modified: true,
+ cache: make(map[interface{}]interface{}),
+ }
+}
+
+// Modified returns whether the state was modified since the last checkpoint.
+func (s *State) Modified() bool {
+ return s.modified
+}
+
+// Lock acquires the state lock.
+func (s *State) Lock() {
+ s.mu.Lock()
+ atomic.AddInt32(&s.muC, 1)
+}
+
+func (s *State) reading() {
+ if atomic.LoadInt32(&s.muC) != 1 {
+ panic("internal error: accessing state without lock")
+ }
+}
+
+func (s *State) writing() {
+ s.modified = true
+ if atomic.LoadInt32(&s.muC) != 1 {
+ panic("internal error: accessing state without lock")
+ }
+}
+
+func (s *State) unlock() {
+ atomic.AddInt32(&s.muC, -1)
+ s.mu.Unlock()
+}
+
+type marshalledState struct {
+ Data map[string]*json.RawMessage `json:"data"`
+ Changes map[string]*Change `json:"changes"`
+ Tasks map[string]*Task `json:"tasks"`
+
+ LastChangeId int `json:"last-change-id"`
+ LastTaskId int `json:"last-task-id"`
+ LastLaneId int `json:"last-lane-id"`
+}
+
+// MarshalJSON makes State a json.Marshaller
+func (s *State) MarshalJSON() ([]byte, error) {
+ s.reading()
+ return json.Marshal(marshalledState{
+ Data: s.data,
+ Changes: s.changes,
+ Tasks: s.tasks,
+
+ LastTaskId: s.lastTaskId,
+ LastChangeId: s.lastChangeId,
+ LastLaneId: s.lastLaneId,
+ })
+}
+
+// UnmarshalJSON makes State a json.Unmarshaller
+func (s *State) UnmarshalJSON(data []byte) error {
+ s.writing()
+ var unmarshalled marshalledState
+ err := json.Unmarshal(data, &unmarshalled)
+ if err != nil {
+ return err
+ }
+ s.data = unmarshalled.Data
+ s.changes = unmarshalled.Changes
+ s.tasks = unmarshalled.Tasks
+ s.lastChangeId = unmarshalled.LastChangeId
+ s.lastTaskId = unmarshalled.LastTaskId
+ s.lastLaneId = unmarshalled.LastLaneId
+ // backlink state again
+ for _, t := range s.tasks {
+ t.state = s
+ }
+ for _, chg := range s.changes {
+ chg.state = s
+ chg.finishUnmarshal()
+ }
+ return nil
+}
+
+func (s *State) checkpointData() []byte {
+ data, err := json.Marshal(s)
+ if err != nil {
+ // this shouldn't happen, because the actual delicate serializing happens at various Set()s
+ logger.Panicf("internal error: could not marshal state for checkpointing: %v", err)
+ }
+ return data
+}
+
+// unlock checkpoint retry parameters (5 mins of retries by default)
+var (
+ unlockCheckpointRetryMaxTime = 5 * time.Minute
+ unlockCheckpointRetryInterval = 3 * time.Second
+)
+
+// Unlock releases the state lock and checkpoints the state.
+// It does not return until the state is correctly checkpointed.
+// After too many unsuccessful checkpoint attempts, it panics.
+func (s *State) Unlock() {
+ defer s.unlock()
+
+ if !s.modified || s.backend == nil {
+ return
+ }
+
+ data := s.checkpointData()
+ var err error
+ start := time.Now()
+ for time.Since(start) <= unlockCheckpointRetryMaxTime {
+ if err = s.backend.Checkpoint(data); err == nil {
+ s.modified = false
+ return
+ }
+ time.Sleep(unlockCheckpointRetryInterval)
+ }
+ logger.Panicf("cannot checkpoint even after %v of retries every %v: %v", unlockCheckpointRetryMaxTime, unlockCheckpointRetryInterval, err)
+}
+
+// EnsureBefore asks for an ensure pass to happen sooner within duration from now.
+func (s *State) EnsureBefore(d time.Duration) {
+ if s.backend != nil {
+ s.backend.EnsureBefore(d)
+ }
+}
+
+// RequestRestart asks for a restart of the managing process.
+func (s *State) RequestRestart(t RestartType) {
+ if s.backend != nil {
+ s.backend.RequestRestart(t)
+ }
+}
+
+// ErrNoState represents the case of no state entry for a given key.
+var ErrNoState = errors.New("no state entry for key")
+
+// Get unmarshals the stored value associated with the provided key
+// into the value parameter.
+// It returns ErrNoState if there is no entry for key.
+func (s *State) Get(key string, value interface{}) error {
+ s.reading()
+ return s.data.get(key, value)
+}
+
+// Set associates value with key for future consulting by managers.
+// The provided value must properly marshal and unmarshal with encoding/json.
+func (s *State) Set(key string, value interface{}) {
+ s.writing()
+ s.data.set(key, value)
+}
+
+// Cached returns the cached value associated with the provided key.
+// It returns nil if there is no entry for key.
+func (s *State) Cached(key interface{}) interface{} {
+ s.reading()
+ return s.cache[key]
+}
+
+// Cache associates value with key for future consulting by managers.
+// The cached value is not persisted.
+func (s *State) Cache(key, value interface{}) {
+ s.reading() // Doesn't touch persisted data.
+ if value == nil {
+ delete(s.cache, key)
+ } else {
+ s.cache[key] = value
+ }
+}
+
+// NewChange adds a new change to the state.
+func (s *State) NewChange(kind, summary string) *Change {
+ s.writing()
+ s.lastChangeId++
+ id := strconv.Itoa(s.lastChangeId)
+ chg := newChange(s, id, kind, summary)
+ s.changes[id] = chg
+ return chg
+}
+
+// NewLane creates a new lane in the state.
+func (s *State) NewLane() int {
+ s.writing()
+ s.lastLaneId++
+ return s.lastLaneId
+}
+
+// Changes returns all changes currently known to the state.
+func (s *State) Changes() []*Change {
+ s.reading()
+ res := make([]*Change, 0, len(s.changes))
+ for _, chg := range s.changes {
+ res = append(res, chg)
+ }
+ return res
+}
+
+// Change returns the change for the given ID.
+func (s *State) Change(id string) *Change {
+ s.reading()
+ return s.changes[id]
+}
+
+// NewTask creates a new task.
+// It usually will be registered with a Change using AddTask or
+// through a TaskSet.
+func (s *State) NewTask(kind, summary string) *Task {
+ s.writing()
+ s.lastTaskId++
+ id := strconv.Itoa(s.lastTaskId)
+ t := newTask(s, id, kind, summary)
+ s.tasks[id] = t
+ return t
+}
+
+// Tasks returns all tasks currently known to the state and linked to changes.
+func (s *State) Tasks() []*Task {
+ s.reading()
+ res := make([]*Task, 0, len(s.tasks))
+ for _, t := range s.tasks {
+ if t.Change() == nil { // skip unlinked tasks
+ continue
+ }
+ res = append(res, t)
+ }
+ return res
+}
+
+// Task returns the task for the given ID if the task has been linked to a change.
+func (s *State) Task(id string) *Task {
+ s.reading()
+ t := s.tasks[id]
+ if t == nil || t.Change() == nil {
+ return nil
+ }
+ return t
+}
+
+// TaskCount returns the number of tasks that currently exist in the state,
+// whether linked to a change or not.
+func (s *State) TaskCount() int {
+ s.reading()
+ return len(s.tasks)
+}
+
+func (s *State) tasksIn(tids []string) []*Task {
+ res := make([]*Task, len(tids))
+ for i, tid := range tids {
+ res[i] = s.tasks[tid]
+ }
+ return res
+}
+
+// Prune removes changes that became ready for more than pruneWait
+// and aborts tasks spawned for more than abortWait.
+// It also removes tasks unlinked to changes after pruneWait.
+func (s *State) Prune(pruneWait, abortWait time.Duration) {
+ now := time.Now()
+ pruneLimit := now.Add(-pruneWait)
+ abortLimit := now.Add(-abortWait)
+ for _, chg := range s.Changes() {
+ spawnTime := chg.SpawnTime()
+ readyTime := chg.ReadyTime()
+ if readyTime.IsZero() {
+ if spawnTime.Before(pruneLimit) && len(chg.Tasks()) == 0 {
+ chg.Abort()
+ delete(s.changes, chg.ID())
+ } else if spawnTime.Before(abortLimit) {
+ chg.Abort()
+ }
+ continue
+ }
+ if readyTime.Before(pruneLimit) {
+ s.writing()
+ for _, t := range chg.Tasks() {
+ delete(s.tasks, t.ID())
+ }
+ delete(s.changes, chg.ID())
+ }
+ }
+ for tid, t := range s.tasks {
+ // TODO: this could be done more aggressively
+ if t.Change() == nil && t.SpawnTime().Before(pruneLimit) {
+ s.writing()
+ delete(s.tasks, tid)
+ }
+ }
+}
+
+// ReadState returns the state deserialized from r.
+func ReadState(backend Backend, r io.Reader) (*State, error) {
+ s := new(State)
+ s.Lock()
+ defer s.unlock()
+ d := json.NewDecoder(r)
+ err := d.Decode(&s)
+ if err != nil {
+ return nil, err
+ }
+ s.backend = backend
+ s.modified = false
+ s.cache = make(map[interface{}]interface{})
+ return s, err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state_test
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+func TestState(t *testing.T) { TestingT(t) }
+
+type stateSuite struct{}
+
+var _ = Suite(&stateSuite{})
+
+type mgrState1 struct {
+ A string
+}
+
+type Count2 struct {
+ B int
+}
+
+type mgrState2 struct {
+ C *Count2
+}
+
+func (ss *stateSuite) TestLockUnlock(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ st.Unlock()
+}
+
+func (ss *stateSuite) TestGetAndSet(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ mSt1 := &mgrState1{A: "foo"}
+ st.Set("mgr1", mSt1)
+ mSt2 := &mgrState2{C: &Count2{B: 42}}
+ st.Set("mgr2", mSt2)
+
+ var mSt1B mgrState1
+ err := st.Get("mgr1", &mSt1B)
+ c.Assert(err, IsNil)
+ c.Check(&mSt1B, DeepEquals, mSt1)
+
+ var mSt2B mgrState2
+ err = st.Get("mgr2", &mSt2B)
+ c.Assert(err, IsNil)
+ c.Check(&mSt2B, DeepEquals, mSt2)
+}
+
+func (ss *stateSuite) TestSetPanic(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ unsupported := struct {
+ Ch chan bool
+ }{}
+ c.Check(func() { st.Set("mgr9", unsupported) }, PanicMatches, `internal error: could not marshal value for state entry "mgr9": json: unsupported type:.*`)
+}
+
+func (ss *stateSuite) TestGetNoState(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ var mSt1B mgrState1
+ err := st.Get("mgr9", &mSt1B)
+ c.Check(err, Equals, state.ErrNoState)
+}
+
+func (ss *stateSuite) TestGetUnmarshalProblem(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ mismatched := struct {
+ A int
+ }{A: 22}
+ st.Set("mgr9", &mismatched)
+
+ var mSt1B mgrState1
+ err := st.Get("mgr9", &mSt1B)
+ c.Check(err, ErrorMatches, `internal error: could not unmarshal state entry "mgr9": json: cannot unmarshal .*`)
+}
+
+func (ss *stateSuite) TestCache(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ type key1 struct{}
+ type key2 struct{}
+
+ c.Assert(st.Cached(key1{}), Equals, nil)
+
+ st.Cache(key1{}, "value1")
+ st.Cache(key2{}, "value2")
+ c.Assert(st.Cached(key1{}), Equals, "value1")
+ c.Assert(st.Cached(key2{}), Equals, "value2")
+
+ st.Cache(key1{}, nil)
+ c.Assert(st.Cached(key1{}), Equals, nil)
+
+ _, ok := st.Cached("key3").(string)
+ c.Assert(ok, Equals, false)
+}
+
+type fakeStateBackend struct {
+ checkpoints [][]byte
+ error func() error
+ ensureBefore time.Duration
+ restartRequested bool
+}
+
+func (b *fakeStateBackend) Checkpoint(data []byte) error {
+ b.checkpoints = append(b.checkpoints, data)
+ if b.error != nil {
+ return b.error()
+ }
+ return nil
+}
+
+func (b *fakeStateBackend) EnsureBefore(d time.Duration) {
+ b.ensureBefore = d
+}
+
+func (b *fakeStateBackend) RequestRestart(t state.RestartType) {
+ b.restartRequested = true
+}
+
+func (ss *stateSuite) TestImplicitCheckpointAndRead(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ st.Set("v", 1)
+ mSt1 := &mgrState1{A: "foo"}
+ st.Set("mgr1", mSt1)
+ mSt2 := &mgrState2{C: &Count2{B: 42}}
+ st.Set("mgr2", mSt2)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+ c.Assert(st2.Modified(), Equals, false)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ var v int
+ err = st2.Get("v", &v)
+ c.Assert(err, IsNil)
+ c.Check(v, Equals, 1)
+
+ var mSt1B mgrState1
+ err = st2.Get("mgr1", &mSt1B)
+ c.Assert(err, IsNil)
+ c.Check(&mSt1B, DeepEquals, mSt1)
+
+ var mSt2B mgrState2
+ err = st2.Get("mgr2", &mSt2B)
+ c.Assert(err, IsNil)
+ c.Check(&mSt2B, DeepEquals, mSt2)
+}
+
+func (ss *stateSuite) TestImplicitCheckpointRetry(c *C) {
+ restore := state.MockCheckpointRetryDelay(2*time.Millisecond, 1*time.Second)
+ defer restore()
+
+ retries := 0
+ boom := errors.New("boom")
+ error := func() error {
+ retries++
+ if retries == 2 {
+ return nil
+ }
+ return boom
+ }
+ b := &fakeStateBackend{error: error}
+ st := state.New(b)
+ st.Lock()
+
+ // implicit checkpoint will retry
+ st.Unlock()
+
+ c.Check(retries, Equals, 2)
+}
+
+func (ss *stateSuite) TestImplicitCheckpointPanicsAfterFailedRetries(c *C) {
+ restore := state.MockCheckpointRetryDelay(2*time.Millisecond, 80*time.Millisecond)
+ defer restore()
+
+ boom := errors.New("boom")
+ retries := 0
+ errFn := func() error {
+ retries++
+ return boom
+ }
+ b := &fakeStateBackend{error: errFn}
+ st := state.New(b)
+ st.Lock()
+
+ // implicit checkpoint will panic after all failed retries
+ t0 := time.Now()
+ c.Check(func() { st.Unlock() }, PanicMatches, "cannot checkpoint even after 80ms of retries every 2ms: boom")
+ // we did at least a couple
+ c.Check(retries > 2, Equals, true, Commentf("expected more than 2 retries got %v", retries))
+ c.Check(time.Since(t0) > 80*time.Millisecond, Equals, true)
+}
+
+func (ss *stateSuite) TestImplicitCheckpointModifiedOnly(c *C) {
+ restore := state.MockCheckpointRetryDelay(2*time.Millisecond, 1*time.Second)
+ defer restore()
+
+ b := &fakeStateBackend{}
+ st := state.New(b)
+ st.Lock()
+ st.Unlock()
+ st.Lock()
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ st.Lock()
+ st.Set("foo", "bar")
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 2)
+}
+
+func (ss *stateSuite) TestNewChangeAndChanges(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg1 := st.NewChange("install", "...")
+ chg2 := st.NewChange("remove", "...")
+
+ chgs := st.Changes()
+ c.Check(chgs, HasLen, 2)
+
+ expected := map[string]*state.Change{
+ chg1.ID(): chg1,
+ chg2.ID(): chg2,
+ }
+
+ for _, chg := range chgs {
+ c.Check(chg, Equals, expected[chg.ID()])
+ c.Check(st.Change(chg.ID()), Equals, chg)
+ }
+
+ c.Check(st.Change("no-such-id"), IsNil)
+}
+
+func (ss *stateSuite) TestNewChangeAndCheckpoint(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ chg := st.NewChange("install", "summary")
+ c.Assert(chg, NotNil)
+ chgID := chg.ID()
+ chg.Set("a", 1)
+ chg.SetStatus(state.ErrorStatus)
+
+ spawnTime := chg.SpawnTime()
+ readyTime := chg.ReadyTime()
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+ c.Assert(st2, NotNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ chgs := st2.Changes()
+
+ c.Assert(chgs, HasLen, 1)
+
+ chg0 := chgs[0]
+ c.Check(chg0.ID(), Equals, chgID)
+ c.Check(chg0.Kind(), Equals, "install")
+ c.Check(chg0.Summary(), Equals, "summary")
+ c.Check(chg0.SpawnTime().Equal(spawnTime), Equals, true)
+ c.Check(chg0.ReadyTime().Equal(readyTime), Equals, true)
+
+ var v int
+ err = chg0.Get("a", &v)
+ c.Check(err, IsNil)
+ c.Check(v, Equals, 1)
+
+ c.Check(chg0.Status(), Equals, state.ErrorStatus)
+
+ select {
+ case <-chg0.Ready():
+ default:
+ c.Errorf("Change didn't preserve Ready channel closed after deserialization")
+ }
+}
+
+func (ss *stateSuite) TestNewChangeAndCheckpointTaskDerivedStatus(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ chg := st.NewChange("install", "summary")
+ c.Assert(chg, NotNil)
+ chgID := chg.ID()
+
+ t1 := st.NewTask("download", "1...")
+ t1.SetStatus(state.DoneStatus)
+ chg.AddTask(t1)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ chgs := st2.Changes()
+
+ c.Assert(chgs, HasLen, 1)
+
+ chg0 := chgs[0]
+ c.Check(chg0.ID(), Equals, chgID)
+ c.Check(chg0.Status(), Equals, state.DoneStatus)
+
+ select {
+ case <-chg0.Ready():
+ default:
+ c.Errorf("Change didn't preserve Ready channel closed after deserialization")
+ }
+}
+
+func (ss *stateSuite) TestNewTaskAndCheckpoint(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ chg := st.NewChange("install", "summary")
+ c.Assert(chg, NotNil)
+
+ t1 := st.NewTask("download", "1...")
+ chg.AddTask(t1)
+ t1ID := t1.ID()
+ t1.Set("a", 1)
+ t1.SetStatus(state.DoneStatus)
+ t1.SetProgress("snap", 5, 10)
+ t1.JoinLane(42)
+ t1.JoinLane(43)
+
+ t2 := st.NewTask("inst", "2...")
+ chg.AddTask(t2)
+ t2ID := t2.ID()
+ t2.WaitFor(t1)
+ schedule := time.Now().Add(time.Hour)
+ t2.At(schedule)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+ c.Assert(st2, NotNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ chgs := st2.Changes()
+ c.Assert(chgs, HasLen, 1)
+ chg0 := chgs[0]
+
+ tasks0 := make(map[string]*state.Task)
+ for _, t := range chg0.Tasks() {
+ tasks0[t.ID()] = t
+ }
+ c.Assert(tasks0, HasLen, 2)
+
+ task0_1 := tasks0[t1ID]
+ c.Check(task0_1.ID(), Equals, t1ID)
+ c.Check(task0_1.Kind(), Equals, "download")
+ c.Check(task0_1.Summary(), Equals, "1...")
+ c.Check(task0_1.Change(), Equals, chg0)
+
+ var v int
+ err = task0_1.Get("a", &v)
+ c.Check(err, IsNil)
+ c.Check(v, Equals, 1)
+
+ c.Check(task0_1.Status(), Equals, state.DoneStatus)
+
+ _, cur, tot := task0_1.Progress()
+ c.Check(cur, Equals, 5)
+ c.Check(tot, Equals, 10)
+
+ c.Assert(task0_1.Lanes(), DeepEquals, []int{42, 43})
+
+ task0_2 := tasks0[t2ID]
+ c.Check(task0_2.WaitTasks(), DeepEquals, []*state.Task{task0_1})
+
+ c.Check(task0_1.HaltTasks(), DeepEquals, []*state.Task{task0_2})
+
+ tasks2 := make(map[string]*state.Task)
+ for _, t := range st2.Tasks() {
+ tasks2[t.ID()] = t
+ }
+ c.Assert(tasks2, HasLen, 2)
+
+ c.Check(task0_1.AtTime().IsZero(), Equals, true)
+ c.Check(task0_2.AtTime().Equal(schedule), Equals, true)
+}
+
+func (ss *stateSuite) TestEmptyStateDataAndCheckpointReadAndSet(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ chg := st.NewChange("install", "summary")
+ c.Assert(chg, NotNil)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+ c.Assert(st2, NotNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ // no crash
+ st2.Set("a", 1)
+}
+
+func (ss *stateSuite) TestEmptyTaskAndChangeDataAndCheckpointReadAndSet(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ t1 := st.NewTask("1...", "...")
+ t1ID := t1.ID()
+ chg := st.NewChange("chg", "...")
+ chgID := chg.ID()
+ chg.AddTask(t1)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+ c.Assert(st2, NotNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ chg2 := st2.Change(chgID)
+ t1_2 := st2.Task(t1ID)
+ c.Assert(t1_2, NotNil)
+
+ // no crash
+ chg2.Set("c", 1)
+ // no crash either
+ t1_2.Set("t", 1)
+}
+
+func (ss *stateSuite) TestEnsureBefore(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+
+ st.EnsureBefore(10 * time.Second)
+
+ c.Check(b.ensureBefore, Equals, 10*time.Second)
+}
+
+func (ss *stateSuite) TestCheckpointPreserveLastIds(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ st.NewChange("install", "...")
+ st.NewTask("download", "...")
+ st.NewTask("download", "...")
+
+ c.Assert(st.NewLane(), Equals, 1)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ c.Assert(st2.NewTask("download", "...").ID(), Equals, "3")
+ c.Assert(st2.NewChange("install", "...").ID(), Equals, "2")
+
+ c.Assert(st2.NewLane(), Equals, 2)
+
+}
+
+func (ss *stateSuite) TestCheckpointPreserveCleanStatus(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ chg := st.NewChange("install", "...")
+ t := st.NewTask("download", "...")
+ chg.AddTask(t)
+ t.SetStatus(state.DoneStatus)
+ t.SetClean()
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ chg2 := st2.Change(chg.ID())
+ t2 := st2.Task(t.ID())
+
+ c.Assert(chg2.IsClean(), Equals, true)
+ c.Assert(t2.IsClean(), Equals, true)
+}
+
+func (ss *stateSuite) TestNewTaskAndTasks(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg1 := st.NewChange("install", "...")
+ t11 := st.NewTask("check", "...")
+ chg1.AddTask(t11)
+ t12 := st.NewTask("inst", "...")
+ chg1.AddTask(t12)
+
+ chg2 := st.NewChange("remove", "...")
+ t21 := st.NewTask("check", "...")
+ t22 := st.NewTask("rm", "...")
+ chg2.AddTask(t21)
+ chg2.AddTask(t22)
+
+ tasks := st.Tasks()
+ c.Check(tasks, HasLen, 4)
+
+ expected := map[string]*state.Task{
+ t11.ID(): t11,
+ t12.ID(): t12,
+ t21.ID(): t21,
+ t22.ID(): t22,
+ }
+
+ for _, t := range tasks {
+ c.Check(t, Equals, expected[t.ID()])
+ }
+}
+
+func (ss *stateSuite) TestTaskNoTask(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ c.Check(st.Task("1"), IsNil)
+}
+
+func (ss *stateSuite) TestNewTaskHiddenUntilLinked(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("check", "...")
+
+ tasks := st.Tasks()
+ c.Check(tasks, HasLen, 0)
+
+ c.Check(st.Task(t1.ID()), IsNil)
+}
+
+func (ss *stateSuite) TestMethodEntrance(c *C) {
+ st := state.New(&fakeStateBackend{})
+
+ // Reset modified flag.
+ st.Lock()
+ st.Unlock()
+
+ writes := []func(){
+ func() { st.Set("foo", 1) },
+ func() { st.NewChange("install", "...") },
+ func() { st.NewTask("download", "...") },
+ func() { st.UnmarshalJSON(nil) },
+ func() { st.NewLane() },
+ }
+
+ reads := []func(){
+ func() { st.Get("foo", nil) },
+ func() { st.Cached("foo") },
+ func() { st.Cache("foo", 1) },
+ func() { st.Changes() },
+ func() { st.Change("foo") },
+ func() { st.Tasks() },
+ func() { st.Task("foo") },
+ func() { st.MarshalJSON() },
+ func() { st.Prune(time.Hour, time.Hour) },
+ func() { st.TaskCount() },
+ }
+
+ for i, f := range reads {
+ c.Logf("Testing read function #%d", i)
+ c.Assert(f, PanicMatches, "internal error: accessing state without lock")
+ c.Assert(st.Modified(), Equals, false)
+ }
+
+ for i, f := range writes {
+ st.Lock()
+ st.Unlock()
+ c.Assert(st.Modified(), Equals, false)
+
+ c.Logf("Testing write function #%d", i)
+ c.Assert(f, PanicMatches, "internal error: accessing state without lock")
+ c.Assert(st.Modified(), Equals, true)
+ }
+}
+
+func (ss *stateSuite) TestPrune(c *C) {
+ st := state.New(&fakeStateBackend{})
+ st.Lock()
+ defer st.Unlock()
+
+ now := time.Now()
+ pruneWait := 1 * time.Hour
+ abortWait := 3 * time.Hour
+
+ unset := time.Time{}
+
+ t1 := st.NewTask("foo", "...")
+ t2 := st.NewTask("foo", "...")
+ t3 := st.NewTask("foo", "...")
+ t4 := st.NewTask("foo", "...")
+
+ chg1 := st.NewChange("abort", "...")
+ chg1.AddTask(t1)
+ state.MockChangeTimes(chg1, now.Add(-abortWait), unset)
+
+ chg2 := st.NewChange("prune", "...")
+ chg2.AddTask(t2)
+ c.Assert(chg2.Status(), Equals, state.DoStatus)
+ state.MockChangeTimes(chg2, now.Add(-pruneWait), now.Add(-pruneWait))
+
+ chg3 := st.NewChange("ready-but-recent", "...")
+ chg3.AddTask(t3)
+ state.MockChangeTimes(chg3, now.Add(-pruneWait), now.Add(-pruneWait/2))
+
+ chg4 := st.NewChange("old-but-not-ready", "...")
+ chg4.AddTask(t4)
+ state.MockChangeTimes(chg4, now.Add(-pruneWait/2), unset)
+
+ // unlinked task
+ t5 := st.NewTask("unliked", "...")
+ c.Check(st.Task(t5.ID()), IsNil)
+ state.MockTaskTimes(t5, now.Add(-pruneWait), now.Add(-pruneWait))
+
+ st.Prune(pruneWait, abortWait)
+
+ c.Assert(st.Change(chg1.ID()), Equals, chg1)
+ c.Assert(st.Change(chg2.ID()), IsNil)
+ c.Assert(st.Change(chg3.ID()), Equals, chg3)
+ c.Assert(st.Change(chg4.ID()), Equals, chg4)
+
+ c.Assert(st.Task(t1.ID()), Equals, t1)
+ c.Assert(st.Task(t2.ID()), IsNil)
+ c.Assert(st.Task(t3.ID()), Equals, t3)
+ c.Assert(st.Task(t4.ID()), Equals, t4)
+
+ c.Assert(chg1.Status(), Equals, state.HoldStatus)
+ c.Assert(chg3.Status(), Equals, state.DoStatus)
+ c.Assert(chg4.Status(), Equals, state.DoStatus)
+
+ c.Assert(t1.Status(), Equals, state.HoldStatus)
+ c.Assert(t3.Status(), Equals, state.DoStatus)
+ c.Assert(t4.Status(), Equals, state.DoStatus)
+
+ c.Check(st.TaskCount(), Equals, 3)
+}
+
+func (ss *stateSuite) TestPruneEmptyChange(c *C) {
+ // Empty changes are a bit special because they start out on Hold
+ // which is a Ready status, but the change itself is not considered Ready
+ // explicitly because that's how every change that will have tasks added
+ // to it starts their life.
+ st := state.New(&fakeStateBackend{})
+ st.Lock()
+ defer st.Unlock()
+
+ now := time.Now()
+ pruneWait := 1 * time.Hour
+ abortWait := 3 * time.Hour
+
+ chg := st.NewChange("abort", "...")
+ state.MockChangeTimes(chg, now.Add(-pruneWait), time.Time{})
+
+ st.Prune(pruneWait, abortWait)
+ c.Assert(st.Change(chg.ID()), IsNil)
+}
+
+func (ss *stateSuite) TestRequestRestart(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+
+ st.RequestRestart(state.RestartDaemon)
+
+ c.Check(b.restartRequested, Equals, true)
+}
+
+func (ss *stateSuite) TestReadStateInitsCache(c *C) {
+ st, err := state.ReadState(nil, bytes.NewBufferString("{}"))
+ c.Assert(err, IsNil)
+ st.Lock()
+ defer st.Unlock()
+
+ st.Cache("key", "value")
+ c.Assert(st.Cached("key"), Equals, "value")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/logger"
+ "time"
+)
+
+type progress struct {
+ Label string `json:"label"`
+ Done int `json:"done"`
+ Total int `json:"total"`
+}
+
+// Task represents an individual operation to be performed
+// for accomplishing one or more state changes.
+//
+// See Change for more details.
+type Task struct {
+ state *State
+ id string
+ kind string
+ summary string
+ status Status
+ clean bool
+ progress *progress
+ data customData
+ waitTasks []string
+ haltTasks []string
+ lanes []int
+ log []string
+ change string
+
+ spawnTime time.Time
+ readyTime time.Time
+
+ atTime time.Time
+}
+
+func newTask(state *State, id, kind, summary string) *Task {
+ return &Task{
+ state: state,
+ id: id,
+ kind: kind,
+ summary: summary,
+ data: make(customData),
+
+ spawnTime: timeNow(),
+ }
+}
+
+type marshalledTask struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Status Status `json:"status"`
+ Clean bool `json:"clean,omitempty"`
+ Progress *progress `json:"progress,omitempty"`
+ Data map[string]*json.RawMessage `json:"data,omitempty"`
+ WaitTasks []string `json:"wait-tasks,omitempty"`
+ HaltTasks []string `json:"halt-tasks,omitempty"`
+ Lanes []int `json:"lanes,omitempty"`
+ Log []string `json:"log,omitempty"`
+ Change string `json:"change"`
+
+ SpawnTime time.Time `json:"spawn-time"`
+ ReadyTime *time.Time `json:"ready-time,omitempty"`
+
+ AtTime *time.Time `json:"at-time,omitempty"`
+}
+
+// MarshalJSON makes Task a json.Marshaller
+func (t *Task) MarshalJSON() ([]byte, error) {
+ t.state.reading()
+ var readyTime *time.Time
+ if !t.readyTime.IsZero() {
+ readyTime = &t.readyTime
+ }
+ var atTime *time.Time
+ if !t.atTime.IsZero() {
+ atTime = &t.atTime
+ }
+ return json.Marshal(marshalledTask{
+ ID: t.id,
+ Kind: t.kind,
+ Summary: t.summary,
+ Status: t.status,
+ Clean: t.clean,
+ Progress: t.progress,
+ Data: t.data,
+ WaitTasks: t.waitTasks,
+ HaltTasks: t.haltTasks,
+ Lanes: t.lanes,
+ Log: t.log,
+ Change: t.change,
+
+ SpawnTime: t.spawnTime,
+ ReadyTime: readyTime,
+
+ AtTime: atTime,
+ })
+}
+
+// UnmarshalJSON makes Task a json.Unmarshaller
+func (t *Task) UnmarshalJSON(data []byte) error {
+ if t.state != nil {
+ t.state.writing()
+ }
+ var unmarshalled marshalledTask
+ err := json.Unmarshal(data, &unmarshalled)
+ if err != nil {
+ return err
+ }
+ t.id = unmarshalled.ID
+ t.kind = unmarshalled.Kind
+ t.summary = unmarshalled.Summary
+ t.status = unmarshalled.Status
+ t.clean = unmarshalled.Clean
+ t.progress = unmarshalled.Progress
+ custData := unmarshalled.Data
+ if custData == nil {
+ custData = make(customData)
+ }
+ t.data = custData
+ t.waitTasks = unmarshalled.WaitTasks
+ t.haltTasks = unmarshalled.HaltTasks
+ t.lanes = unmarshalled.Lanes
+ t.log = unmarshalled.Log
+ t.change = unmarshalled.Change
+ t.spawnTime = unmarshalled.SpawnTime
+ if unmarshalled.ReadyTime != nil {
+ t.readyTime = *unmarshalled.ReadyTime
+ }
+ if unmarshalled.AtTime != nil {
+ t.atTime = *unmarshalled.AtTime
+ }
+ return nil
+}
+
+// ID returns the individual random key for this task.
+func (t *Task) ID() string {
+ return t.id
+}
+
+// Kind returns the nature of this task for managers to know how to handle it.
+func (t *Task) Kind() string {
+ return t.kind
+}
+
+// Summary returns a summary describing what the task is about.
+func (t *Task) Summary() string {
+ return t.summary
+}
+
+// Status returns the current task status.
+func (t *Task) Status() Status {
+ t.state.reading()
+ if t.status == DefaultStatus {
+ return DoStatus
+ }
+ return t.status
+}
+
+// SetStatus sets the task status, overriding the default behavior (see Status method).
+func (t *Task) SetStatus(new Status) {
+ t.state.writing()
+ old := t.status
+ t.status = new
+ if !old.Ready() && new.Ready() {
+ t.readyTime = timeNow()
+ }
+ chg := t.Change()
+ if chg != nil {
+ chg.taskStatusChanged(t, old, new)
+ }
+}
+
+// IsClean returns whether the task has been cleaned. See SetClean.
+func (t *Task) IsClean() bool {
+ t.state.reading()
+ return t.clean
+}
+
+// SetClean flags the task as clean after any left over data was removed.
+//
+// Cleaning a task must only be done after the change is ready.
+func (t *Task) SetClean() {
+ t.state.writing()
+ if t.clean {
+ return
+ }
+ t.clean = true
+ chg := t.Change()
+ if chg != nil {
+ chg.taskCleanChanged()
+ }
+}
+
+// State returns the system State
+func (t *Task) State() *State {
+ return t.state
+}
+
+// Change returns the change the task is registered with.
+func (t *Task) Change() *Change {
+ t.state.reading()
+ return t.state.changes[t.change]
+}
+
+// Progress returns the current progress for the task.
+// If progress is not explicitly set, it returns
+// (0, 1) if the status is DoStatus and (1, 1) otherwise.
+func (t *Task) Progress() (label string, done, total int) {
+ t.state.reading()
+ if t.progress == nil {
+ if t.Status() == DoStatus {
+ return "", 0, 1
+ }
+ return "", 1, 1
+ }
+ return t.progress.Label, t.progress.Done, t.progress.Total
+}
+
+// SetProgress sets the task progress to cur out of total steps.
+func (t *Task) SetProgress(label string, done, total int) {
+ // Only mark state for checkpointing if progress is final.
+ if total > 0 && done == total {
+ t.state.writing()
+ } else {
+ t.state.reading()
+ }
+ if total <= 0 || done > total {
+ // Doing math wrong is easy. Be conservative.
+ t.progress = nil
+ } else {
+ t.progress = &progress{Label: label, Done: done, Total: total}
+ }
+}
+
+// SpawnTime returns the time when the change was created.
+func (t *Task) SpawnTime() time.Time {
+ t.state.reading()
+ return t.spawnTime
+}
+
+// ReadyTime returns the time when the change became ready.
+func (t *Task) ReadyTime() time.Time {
+ t.state.reading()
+ return t.readyTime
+}
+
+// AtTime returns the time at which the task is scheduled to run. A zero time means no special schedule, i.e. run as soon as prerequisites are met.
+func (t *Task) AtTime() time.Time {
+ t.state.reading()
+ return t.atTime
+}
+
+const (
+ // Messages logged in tasks are guaranteed to use the time formatted
+ // per RFC3339 plus the following strings as a prefix, so these may
+ // be handled programmatically and parsed or stripped for presentation.
+ LogInfo = "INFO"
+ LogError = "ERROR"
+)
+
+var timeNow = time.Now
+
+func MockTime(now time.Time) (restore func()) {
+ timeNow = func() time.Time { return now }
+ return func() { timeNow = time.Now }
+}
+
+func (t *Task) addLog(kind, format string, args []interface{}) {
+ if len(t.log) > 9 {
+ copy(t.log, t.log[len(t.log)-9:])
+ t.log = t.log[:9]
+ }
+
+ tstr := timeNow().Format(time.RFC3339)
+ msg := fmt.Sprintf(tstr+" "+kind+" "+format, args...)
+ t.log = append(t.log, msg)
+ logger.Debugf(msg)
+}
+
+// Log returns the most recent messages logged into the task.
+//
+// Only the most recent entries logged are returned, potentially with
+// different behavior for different task statuses. How many entries
+// are returned is an implementation detail and may change over time.
+//
+// Messages are prefixed with one of the known message kinds.
+// See details about LogInfo and LogError.
+//
+// The returned slice should not be read from without the
+// state lock held, and should not be written to.
+func (t *Task) Log() []string {
+ t.state.reading()
+ return t.log
+}
+
+// Logf logs information about the progress of the task.
+func (t *Task) Logf(format string, args ...interface{}) {
+ t.state.writing()
+ t.addLog(LogInfo, format, args)
+}
+
+// Errorf logs error information about the progress of the task.
+func (t *Task) Errorf(format string, args ...interface{}) {
+ t.state.writing()
+ t.addLog(LogError, format, args)
+}
+
+// Set associates value with key for future consulting by managers.
+// The provided value must properly marshal and unmarshal with encoding/json.
+func (t *Task) Set(key string, value interface{}) {
+ t.state.writing()
+ t.data.set(key, value)
+}
+
+// Get unmarshals the stored value associated with the provided key
+// into the value parameter.
+func (t *Task) Get(key string, value interface{}) error {
+ t.state.reading()
+ return t.data.get(key, value)
+}
+
+// Clear disassociates the value from key.
+func (t *Task) Clear(key string) {
+ t.state.writing()
+ delete(t.data, key)
+}
+
+func addOnce(set []string, s string) []string {
+ for _, cur := range set {
+ if s == cur {
+ return set
+ }
+ }
+ return append(set, s)
+}
+
+// WaitFor registers another task as a requirement for t to make progress.
+func (t *Task) WaitFor(another *Task) {
+ t.state.writing()
+ t.waitTasks = addOnce(t.waitTasks, another.id)
+ another.haltTasks = addOnce(another.haltTasks, t.id)
+}
+
+// WaitAll registers all the tasks in the set as a requirement for t
+// to make progress.
+func (t *Task) WaitAll(ts *TaskSet) {
+ for _, req := range ts.tasks {
+ t.WaitFor(req)
+ }
+}
+
+// WaitTasks returns the list of tasks registered for t to wait for.
+func (t *Task) WaitTasks() []*Task {
+ t.state.reading()
+ return t.state.tasksIn(t.waitTasks)
+}
+
+// HaltTasks returns the list of tasks registered to wait for t.
+func (t *Task) HaltTasks() []*Task {
+ t.state.reading()
+ return t.state.tasksIn(t.haltTasks)
+}
+
+// Lanes returns the lanes the task is in.
+func (t *Task) Lanes() []int {
+ t.state.reading()
+ if len(t.lanes) == 0 {
+ return []int{0}
+ }
+ return t.lanes
+}
+
+// JoinLane registers the task in the provided lane. Tasks in different lanes
+// abort independently on errors. See Change.AbortLane for details.
+func (t *Task) JoinLane(lane int) {
+ t.state.writing()
+ t.lanes = append(t.lanes, lane)
+}
+
+// At schedules the task, if it's not ready, to happen no earlier than when, if when is the zero time any previous special scheduling is suppressed.
+func (t *Task) At(when time.Time) {
+ t.state.writing()
+ iszero := when.IsZero()
+ if t.Status().Ready() && !iszero {
+ return
+ }
+ t.atTime = when
+ if !iszero {
+ d := when.Sub(timeNow())
+ if d < 0 {
+ d = 0
+ }
+ t.state.EnsureBefore(d)
+ }
+}
+
+// A TaskSet holds a set of tasks.
+type TaskSet struct {
+ tasks []*Task
+}
+
+// NewTaskSet returns a new TaskSet comprising the given tasks.
+func NewTaskSet(tasks ...*Task) *TaskSet {
+ return &TaskSet{tasks}
+}
+
+// WaitFor registers a task as a requirement for the tasks in the set
+// to make progress.
+func (ts TaskSet) WaitFor(another *Task) {
+ for _, t := range ts.tasks {
+ t.WaitFor(another)
+ }
+}
+
+// WaitAll registers all the tasks in the argument set as requirements for ts
+// the target set to make progress.
+func (ts *TaskSet) WaitAll(anotherTs *TaskSet) {
+ for _, req := range anotherTs.tasks {
+ ts.WaitFor(req)
+ }
+}
+
+// AddTask adds the the task to the task set.
+func (ts *TaskSet) AddTask(task *Task) {
+ for _, t := range ts.tasks {
+ if t == task {
+ return
+ }
+ }
+ ts.tasks = append(ts.tasks, task)
+}
+
+// AddAll adds all the tasks in the argument set to the target set ts.
+func (ts *TaskSet) AddAll(anotherTs *TaskSet) {
+ for _, t := range anotherTs.tasks {
+ ts.AddTask(t)
+ }
+}
+
+// JoinLane adds all the tasks in the current taskset to the given lane
+func (ts *TaskSet) JoinLane(lane int) {
+ for _, t := range ts.tasks {
+ t.JoinLane(lane)
+ }
+}
+
+// Tasks returns the tasks in the task set.
+func (ts TaskSet) Tasks() []*Task {
+ // Return something mutable, just like every other Tasks method.
+ return append([]*Task(nil), ts.tasks...)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state_test
+
+import (
+ "encoding/json"
+ "fmt"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/testutil"
+ "time"
+)
+
+type taskSuite struct{}
+
+var _ = Suite(&taskSuite{})
+
+func (ts *taskSuite) TestNewTask(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ c.Check(t.Kind(), Equals, "download")
+ c.Check(t.Summary(), Equals, "1...")
+}
+
+func (cs *taskSuite) TestReadyTime(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.NewTask("download", "summary...")
+
+ now := time.Now()
+
+ t := task.SpawnTime()
+ c.Check(t.After(now.Add(-5*time.Second)), Equals, true)
+ c.Check(t.Before(now.Add(5*time.Second)), Equals, true)
+
+ c.Check(task.ReadyTime().IsZero(), Equals, true)
+
+ task.SetStatus(state.DoneStatus)
+
+ t = task.ReadyTime()
+ c.Check(t.After(now.Add(-5*time.Second)), Equals, true)
+ c.Check(t.Before(now.Add(5*time.Second)), Equals, true)
+}
+
+func (ts *taskSuite) TestGetSet(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ t.Set("a", 1)
+
+ var v int
+ err := t.Get("a", &v)
+ c.Assert(err, IsNil)
+ c.Check(v, Equals, 1)
+}
+
+func (ts *taskSuite) TestClear(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ t.Set("a", 1)
+
+ var v int
+ err := t.Get("a", &v)
+ c.Assert(err, IsNil)
+ c.Check(v, Equals, 1)
+
+ t.Clear("a")
+
+ c.Check(t.Get("a", &v), Equals, state.ErrNoState)
+}
+
+func (ts *taskSuite) TestStatusAndSetStatus(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ c.Check(t.Status(), Equals, state.DoStatus)
+
+ t.SetStatus(state.DoneStatus)
+
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
+
+func (ts *taskSuite) TestIsCleanAndSetClean(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ c.Check(t.IsClean(), Equals, false)
+
+ t.SetStatus(state.DoneStatus)
+ t.SetClean()
+
+ c.Check(t.IsClean(), Equals, true)
+}
+
+func jsonStr(m json.Marshaler) string {
+ data, err := m.MarshalJSON()
+ if err != nil {
+ panic(err)
+ }
+ return string(data)
+}
+
+func (ts *taskSuite) TestProgressAndSetProgress(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ t.SetProgress("snap", 2, 99)
+ label, cur, tot := t.Progress()
+ c.Check(label, Equals, "snap")
+ c.Check(cur, Equals, 2)
+ c.Check(tot, Equals, 99)
+
+ t.SetProgress("", 0, 0)
+ label, cur, tot = t.Progress()
+ c.Check(label, Equals, "")
+ c.Check(cur, Equals, 0)
+ c.Check(tot, Equals, 1)
+ c.Check(jsonStr(t), Not(testutil.Contains), "progress")
+
+ t.SetProgress("", 0, -1)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 0)
+ c.Check(tot, Equals, 1)
+ c.Check(jsonStr(t), Not(testutil.Contains), "progress")
+
+ t.SetProgress("", 0, -1)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 0)
+ c.Check(tot, Equals, 1)
+ c.Check(jsonStr(t), Not(testutil.Contains), "progress")
+
+ t.SetProgress("", 2, 1)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 0)
+ c.Check(tot, Equals, 1)
+ c.Check(jsonStr(t), Not(testutil.Contains), "progress")
+
+ t.SetProgress("", 42, 42)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 42)
+ c.Check(tot, Equals, 42)
+}
+
+func (ts *taskSuite) TestProgressDefaults(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ c.Check(t.Status(), Equals, state.DoStatus)
+ _, cur, tot := t.Progress()
+ c.Check(cur, Equals, 0)
+ c.Check(tot, Equals, 1)
+
+ t.SetStatus(state.DoStatus)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 0)
+ c.Check(tot, Equals, 1)
+
+ t.SetStatus(state.DoneStatus)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 1)
+ c.Check(tot, Equals, 1)
+
+ t.SetStatus(state.ErrorStatus)
+ _, cur, tot = t.Progress()
+ c.Check(cur, Equals, 1)
+ c.Check(tot, Equals, 1)
+}
+
+func (ts *taskSuite) TestState(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ t := st.NewTask("download", "1...")
+ st.Unlock()
+
+ c.Assert(t.State(), Equals, st)
+}
+
+func (ts *taskSuite) TestTaskMarshalsWaitFor(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("install", "2...")
+ t2.WaitFor(t1)
+
+ d, err := t2.MarshalJSON()
+ c.Assert(err, IsNil)
+
+ needle := fmt.Sprintf(`"wait-tasks":["%s"`, t1.ID())
+ c.Assert(string(d), testutil.Contains, needle)
+}
+
+func (ts *taskSuite) TestTaskWaitFor(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("install", "2...")
+ t2.WaitFor(t1)
+
+ c.Assert(t2.WaitTasks(), DeepEquals, []*state.Task{t1})
+ c.Assert(t1.HaltTasks(), DeepEquals, []*state.Task{t2})
+}
+
+func (ts *taskSuite) TestAt(c *C) {
+ b := new(fakeStateBackend)
+ b.ensureBefore = time.Hour
+ st := state.New(b)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ now := time.Now()
+ restore := state.MockTime(now)
+ defer restore()
+ when := now.Add(10 * time.Second)
+ t.At(when)
+
+ c.Check(t.AtTime().Equal(when), Equals, true)
+ c.Check(b.ensureBefore, Equals, 10*time.Second)
+}
+
+func (ts *taskSuite) TestAtPast(c *C) {
+ b := new(fakeStateBackend)
+ b.ensureBefore = time.Hour
+ st := state.New(b)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ when := time.Now().Add(-10 * time.Second)
+ t.At(when)
+
+ c.Check(t.AtTime().Equal(when), Equals, true)
+ c.Check(b.ensureBefore, Equals, time.Duration(0))
+}
+
+func (ts *taskSuite) TestAtReadyNop(c *C) {
+ b := new(fakeStateBackend)
+ b.ensureBefore = time.Hour
+ st := state.New(b)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+ t.SetStatus(state.DoneStatus)
+
+ when := time.Now().Add(10 * time.Second)
+ t.At(when)
+
+ c.Check(t.AtTime().IsZero(), Equals, true)
+ c.Check(b.ensureBefore, Equals, time.Hour)
+}
+
+func (cs *taskSuite) TestLogf(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ for i := 0; i < 20; i++ {
+ t.Logf("Message #%d", i)
+ }
+
+ log := t.Log()
+ c.Assert(log, HasLen, 10)
+ for i := 0; i < 10; i++ {
+ c.Assert(log[i], Matches, fmt.Sprintf("....-..-..T.* INFO Message #%d", i+10))
+ }
+}
+
+func (cs *taskSuite) TestErrorf(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ t.Errorf("Some %s", "error")
+ c.Assert(t.Log()[0], Matches, "....-..-..T.* ERROR Some error")
+}
+
+func (ts *taskSuite) TestTaskMarshalsLog(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+ t.Logf("foo")
+
+ d, err := t.MarshalJSON()
+ c.Assert(err, IsNil)
+
+ c.Assert(string(d), Matches, `.*"log":\["....-..-..T.* INFO foo"\].*`)
+}
+
+// TODO: Better testing of full task roundtripping via JSON.
+
+func (cs *taskSuite) TestMethodEntrance(c *C) {
+ st := state.New(&fakeStateBackend{})
+ st.Lock()
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("install", "2...")
+ st.Unlock()
+
+ writes := []func(){
+ func() { t1.SetStatus(state.DoneStatus) },
+ func() { t1.SetClean() },
+ func() { t1.Set("a", 1) },
+ func() { t2.WaitFor(t1) },
+ func() { t1.SetProgress("", 2, 2) },
+ func() { t1.Logf("") },
+ func() { t1.Errorf("") },
+ func() { t1.UnmarshalJSON(nil) },
+ func() { t1.SetProgress("", 1, 1) },
+ func() { t1.JoinLane(1) },
+ }
+
+ reads := []func(){
+ func() { t1.Status() },
+ func() { t1.IsClean() },
+ func() { t1.Get("a", nil) },
+ func() { t1.WaitTasks() },
+ func() { t1.HaltTasks() },
+ func() { t1.Progress() },
+ func() { t1.Log() },
+ func() { t1.MarshalJSON() },
+ func() { t1.Progress() },
+ func() { t1.SetProgress("", 0, 1) },
+ func() { t1.Lanes() },
+ }
+
+ for i, f := range reads {
+ c.Logf("Testing read function #%d", i)
+ c.Assert(f, PanicMatches, "internal error: accessing state without lock")
+ c.Assert(st.Modified(), Equals, false)
+ }
+
+ for i, f := range writes {
+ st.Lock()
+ st.Unlock()
+ c.Assert(st.Modified(), Equals, false)
+
+ c.Logf("Testing write function #%d", i)
+ c.Assert(f, PanicMatches, "internal error: accessing state without lock")
+ c.Assert(st.Modified(), Equals, true)
+ }
+}
+
+func (cs *taskSuite) TestNewTaskSet(c *C) {
+ ts0 := state.NewTaskSet()
+ c.Check(ts0.Tasks(), HasLen, 0)
+
+ st := state.New(nil)
+ st.Lock()
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("install", "2...")
+ ts2 := state.NewTaskSet(t1, t2)
+ st.Unlock()
+
+ c.Assert(ts2.Tasks(), DeepEquals, []*state.Task{t1, t2})
+}
+
+func (ts *taskSuite) TestTaskWaitAll(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("install", "2...")
+ t3 := st.NewTask("setup", "3...")
+ t3.WaitAll(state.NewTaskSet(t1, t2))
+
+ c.Assert(t3.WaitTasks(), HasLen, 2)
+ c.Assert(t1.HaltTasks(), DeepEquals, []*state.Task{t3})
+ c.Assert(t2.HaltTasks(), DeepEquals, []*state.Task{t3})
+}
+
+func (ts *taskSuite) TestTaskSetWaitFor(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("install", "2...")
+ t3 := st.NewTask("setup", "3...")
+ ts23 := state.NewTaskSet(t2, t3)
+ ts23.WaitFor(t1)
+
+ c.Assert(t2.WaitTasks(), DeepEquals, []*state.Task{t1})
+ c.Assert(t3.WaitTasks(), DeepEquals, []*state.Task{t1})
+ c.Assert(t1.HaltTasks(), HasLen, 2)
+}
+
+func (ts *taskSuite) TestTaskSetWaitAll(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("check", "2...")
+ t3 := st.NewTask("setup", "3...")
+ t4 := st.NewTask("link", "4...")
+ ts12 := state.NewTaskSet(t1, t2)
+ ts34 := state.NewTaskSet(t3, t4)
+ ts34.WaitAll(ts12)
+
+ c.Assert(t3.WaitTasks(), DeepEquals, []*state.Task{t1, t2})
+ c.Assert(t4.WaitTasks(), DeepEquals, []*state.Task{t1, t2})
+ c.Assert(t1.HaltTasks(), HasLen, 2)
+ c.Assert(t2.HaltTasks(), HasLen, 2)
+}
+
+func (ts *taskSuite) TestTaskSetAddTaskAndAddAll(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t1 := st.NewTask("download", "1...")
+ t2 := st.NewTask("check", "2...")
+ t3 := st.NewTask("setup", "3...")
+ t4 := st.NewTask("link", "4...")
+
+ ts0 := state.NewTaskSet(t1)
+
+ ts0.AddTask(t2)
+ ts0.AddAll(state.NewTaskSet(t3, t4))
+
+ // these do nothing
+ ts0.AddTask(t2)
+ ts0.AddAll(state.NewTaskSet(t3, t4))
+
+ c.Check(ts0.Tasks(), DeepEquals, []*state.Task{t1, t2, t3, t4})
+}
+
+func (ts *taskSuite) TestLanes(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ c.Assert(t.Lanes(), DeepEquals, []int{0})
+ t.JoinLane(1)
+ c.Assert(t.Lanes(), DeepEquals, []int{1})
+ t.JoinLane(2)
+ c.Assert(t.Lanes(), DeepEquals, []int{1, 2})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state
+
+import (
+ "sync"
+ "time"
+
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/logger"
+)
+
+// HandlerFunc is the type of function for the handlers
+type HandlerFunc func(task *Task, tomb *tomb.Tomb) error
+
+// Retry is returned from a handler to signal that is ok to rerun the
+// task at a later point. It's to be used also when a task goroutine
+// is asked to stop through its tomb. After can be used to indicate
+// how much to postpone the retry, 0 (the default) means at the next
+// ensure pass and is what should be used if stopped through its tomb.
+type Retry struct {
+ After time.Duration
+}
+
+func (r *Retry) Error() string {
+ return "task should be retried"
+}
+
+// TaskRunner controls the running of goroutines to execute known task kinds.
+type TaskRunner struct {
+ state *State
+
+ // locking
+ mu sync.Mutex
+ handlers map[string]handlerPair
+ cleanups map[string]HandlerFunc
+ stopped bool
+
+ blocked func(t *Task, running []*Task) bool
+ someBlocked bool
+
+ // go-routines lifecycle
+ tombs map[string]*tomb.Tomb
+}
+
+type handlerPair struct {
+ do, undo HandlerFunc
+}
+
+// NewTaskRunner creates a new TaskRunner
+func NewTaskRunner(s *State) *TaskRunner {
+ return &TaskRunner{
+ state: s,
+ handlers: make(map[string]handlerPair),
+ cleanups: make(map[string]HandlerFunc),
+ tombs: make(map[string]*tomb.Tomb),
+ }
+}
+
+// AddHandler registers the functions to concurrently call for doing and
+// undoing tasks of the given kind. The undo handler may be nil.
+func (r *TaskRunner) AddHandler(kind string, do, undo HandlerFunc) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.handlers[kind] = handlerPair{do, undo}
+}
+
+// AddCleanup registers a function to be called after the change completes,
+// for cleaning up data left behind by tasks of the specified kind.
+// The provided function will be called no matter what the final status of the
+// task is. This mechanism enables keeping data around for a potential undo
+// until there's no more chance of the task being undone.
+//
+// The cleanup function is run concurrently with other cleanup functions,
+// despite any wait ordering between the tasks. If it returns an error,
+// it will be retried later.
+//
+// The handler for tasks of the provided kind must have been previously
+// registered before AddCleanup is called for it.
+func (r *TaskRunner) AddCleanup(kind string, cleanup HandlerFunc) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if _, ok := r.handlers[kind]; !ok {
+ panic("internal error: attempted to register cleanup for unknown task kind")
+ }
+ r.cleanups[kind] = cleanup
+}
+
+// SetBlocked sets a predicate function to decide whether to block a task from running based on the current running tasks. It can be used to control task serialisation.
+func (r *TaskRunner) SetBlocked(pred func(t *Task, running []*Task) bool) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.blocked = pred
+}
+
+// run must be called with the state lock in place
+func (r *TaskRunner) run(t *Task) {
+ var handler HandlerFunc
+ switch t.Status() {
+ case DoStatus:
+ t.SetStatus(DoingStatus)
+ fallthrough
+ case DoingStatus:
+ handler = r.handlers[t.Kind()].do
+
+ case UndoStatus:
+ t.SetStatus(UndoingStatus)
+ fallthrough
+ case UndoingStatus:
+ handler = r.handlers[t.Kind()].undo
+
+ default:
+ panic("internal error: attempted to run task in status " + t.Status().String())
+ }
+ if handler == nil {
+ panic("internal error: attempted to run task with nil handler for status " + t.Status().String())
+ }
+
+ t.At(time.Time{}) // clear schedule
+ tomb := &tomb.Tomb{}
+ r.tombs[t.ID()] = tomb
+ tomb.Go(func() error {
+ // Capture the error result with tomb.Kill so we can
+ // use tomb.Err uniformily to consider both it or a
+ // overriding previous Kill reason.
+ tomb.Kill(handler(t, tomb))
+
+ // Locks must be acquired in the same order everywhere.
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.state.Lock()
+ defer r.state.Unlock()
+
+ delete(r.tombs, t.ID())
+
+ // some tasks were blocked, now there's chance the
+ // blocked predicate will change its value
+ if r.someBlocked {
+ r.state.EnsureBefore(0)
+ }
+
+ switch err := tomb.Err(); x := err.(type) {
+ case *Retry:
+ // Handler asked to be called again later.
+ // TODO Allow postponing retries past the next Ensure.
+ if t.Status() == AbortStatus {
+ // Would work without it but might take two ensures.
+ r.tryUndo(t)
+ } else if x.After != 0 {
+ t.At(timeNow().Add(x.After))
+ }
+ case nil:
+ var next []*Task
+ switch t.Status() {
+ case DoingStatus:
+ t.SetStatus(DoneStatus)
+ fallthrough
+ case DoneStatus:
+ next = t.HaltTasks()
+ case AbortStatus:
+ // It was actually Done if it got here.
+ t.SetStatus(UndoStatus)
+ r.state.EnsureBefore(0)
+ case UndoingStatus:
+ t.SetStatus(UndoneStatus)
+ fallthrough
+ case UndoneStatus:
+ next = t.WaitTasks()
+ }
+ if len(next) > 0 {
+ r.state.EnsureBefore(0)
+ }
+ default:
+ r.abortLanes(t.Change(), t.Lanes())
+ t.SetStatus(ErrorStatus)
+ t.Errorf("%s", err)
+ }
+
+ return nil
+ })
+}
+
+func (r *TaskRunner) clean(t *Task) {
+ if !t.Change().IsReady() {
+ // Whole Change is not ready so don't run cleanups yet.
+ return
+ }
+
+ cleanup, ok := r.cleanups[t.Kind()]
+ if !ok {
+ t.SetClean()
+ return
+ }
+
+ tomb := &tomb.Tomb{}
+ r.tombs[t.ID()] = tomb
+ tomb.Go(func() error {
+ tomb.Kill(cleanup(t, tomb))
+
+ // Locks must be acquired in the same order everywhere.
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.state.Lock()
+ defer r.state.Unlock()
+
+ delete(r.tombs, t.ID())
+
+ if tomb.Err() != nil {
+ logger.Debugf("Cleaning task %s: %s", t.ID(), tomb.Err())
+ } else {
+ t.SetClean()
+ }
+ return nil
+ })
+}
+
+func (r *TaskRunner) abortLanes(chg *Change, lanes []int) {
+ chg.AbortLanes(lanes)
+ ensureScheduled := false
+ for _, t := range chg.Tasks() {
+ status := t.Status()
+ if status == AbortStatus {
+ if tb, ok := r.tombs[t.ID()]; ok {
+ tb.Kill(nil)
+ }
+ }
+ if !ensureScheduled && !status.Ready() {
+ ensureScheduled = true
+ r.state.EnsureBefore(0)
+ }
+ }
+}
+
+// tryUndo replaces the status of a knowingly aborted task.
+func (r *TaskRunner) tryUndo(t *Task) {
+ if t.Status() == AbortStatus && r.handlers[t.Kind()].undo == nil {
+ // Cannot undo but it was stopped in flight.
+ // Hold so it doesn't look like it finished.
+ t.SetStatus(HoldStatus)
+ if len(t.WaitTasks()) > 0 {
+ r.state.EnsureBefore(0)
+ }
+ } else {
+ t.SetStatus(UndoStatus)
+ r.state.EnsureBefore(0)
+ }
+}
+
+// Ensure starts new goroutines for all known tasks with no pending
+// dependencies.
+// Note that Ensure will lock the state.
+func (r *TaskRunner) Ensure() {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if r.stopped {
+ // we are stopping, don't run another ensure
+ return
+ }
+
+ // Locks must be acquired in the same order everywhere.
+ r.state.Lock()
+ defer r.state.Unlock()
+
+ r.someBlocked = false
+ running := make([]*Task, 0, len(r.tombs))
+ for tid := range r.tombs {
+ t := r.state.Task(tid)
+ if t != nil {
+ running = append(running, t)
+ }
+ }
+
+ ensureTime := timeNow()
+ nextTaskTime := time.Time{}
+ for _, t := range r.state.Tasks() {
+ handlers, ok := r.handlers[t.Kind()]
+ if !ok {
+ // Handled by a different runner instance.
+ continue
+ }
+
+ tb := r.tombs[t.ID()]
+
+ if t.Status() == AbortStatus {
+ if tb != nil {
+ tb.Kill(nil)
+ continue
+ }
+ r.tryUndo(t)
+ }
+
+ if tb != nil {
+ // Already being handled.
+ continue
+ }
+
+ status := t.Status()
+ if status.Ready() {
+ if !t.IsClean() {
+ r.clean(t)
+ }
+ continue
+ }
+ if status == UndoStatus && handlers.undo == nil {
+ // Cannot undo. Revert to done status.
+ t.SetStatus(DoneStatus)
+ if len(t.WaitTasks()) > 0 {
+ r.state.EnsureBefore(0)
+ }
+ continue
+ }
+
+ if mustWait(t) {
+ // Dependencies still unhandled.
+ continue
+ }
+
+ // skip tasks scheduled for later and also track the earliest one
+ tWhen := t.AtTime()
+ if !tWhen.IsZero() && ensureTime.Before(tWhen) {
+ if nextTaskTime.IsZero() || nextTaskTime.After(tWhen) {
+ nextTaskTime = tWhen
+ }
+ continue
+ }
+
+ if r.blocked != nil && r.blocked(t, running) {
+ r.someBlocked = true
+ continue
+ }
+
+ logger.Debugf("Running task %s on %s: %s", t.ID(), t.Status(), t.Summary())
+ r.run(t)
+
+ running = append(running, t)
+ }
+
+ // schedule next Ensure no later than the next task time
+ if !nextTaskTime.IsZero() {
+ r.state.EnsureBefore(nextTaskTime.Sub(ensureTime))
+ }
+}
+
+// mustWait returns whether task t must wait for other tasks to be done.
+func mustWait(t *Task) bool {
+ switch t.Status() {
+ case DoStatus:
+ for _, wt := range t.WaitTasks() {
+ if wt.Status() != DoneStatus {
+ return true
+ }
+ }
+ case UndoStatus:
+ for _, ht := range t.HaltTasks() {
+ if !ht.Status().Ready() {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// wait expects to be called with th r.mu lock held
+func (r *TaskRunner) wait() {
+ for len(r.tombs) > 0 {
+ for _, t := range r.tombs {
+ r.mu.Unlock()
+ t.Wait()
+ r.mu.Lock()
+ break
+ }
+ }
+}
+
+// Stop kills all concurrent activities and returns after that's done.
+func (r *TaskRunner) Stop() {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.stopped = true
+
+ for _, tb := range r.tombs {
+ tb.Kill(nil)
+ }
+
+ r.wait()
+}
+
+// Wait waits for all concurrent activities and returns after that's done.
+func (r *TaskRunner) Wait() {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.wait()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state_test
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/tomb.v2"
+
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type taskRunnerSuite struct{}
+
+var _ = Suite(&taskRunnerSuite{})
+
+type stateBackend struct {
+ mu sync.Mutex
+ ensureBefore time.Duration
+ ensureBeforeSeen chan<- bool
+}
+
+func (b *stateBackend) Checkpoint([]byte) error { return nil }
+
+func (b *stateBackend) EnsureBefore(d time.Duration) {
+ b.mu.Lock()
+ if d < b.ensureBefore {
+ b.ensureBefore = d
+ }
+ b.mu.Unlock()
+ if b.ensureBeforeSeen != nil {
+ b.ensureBeforeSeen <- true
+ }
+}
+
+func (b *stateBackend) RequestRestart(t state.RestartType) {}
+
+func ensureChange(c *C, r *state.TaskRunner, sb *stateBackend, chg *state.Change) {
+ for i := 0; i < 10; i++ {
+ sb.ensureBefore = time.Hour
+ r.Ensure()
+ r.Wait()
+ chg.State().Lock()
+ s := chg.Status()
+ chg.State().Unlock()
+ if s.Ready() {
+ return
+ }
+ if sb.ensureBefore > 0 {
+ break
+ }
+ }
+ var statuses []string
+ chg.State().Lock()
+ for _, t := range chg.Tasks() {
+ statuses = append(statuses, t.Summary()+":"+t.Status().String())
+ }
+ chg.State().Unlock()
+ c.Fatalf("Change didn't reach final state without blocking: %s", strings.Join(statuses, " "))
+}
+
+// The result field encodes the expected order in which the task
+// handlers will be called, assuming the provided setup is in place.
+//
+// Setup options:
+// <task>:was-<status> - set task status before calling ensure (must be sensible)
+// <task>:(do|undo)-block - block handler until task tomb dies
+// <task>:(do|undo)-retry - return from handler with with state.Retry
+// <task>:(do|undo)-error - return from handler with an error
+// <task>:...:1,2 - one of the above, and add task to lanes 1 and 2
+// chg:abort - call abort on the change
+//
+// Task wait order: ( t11 | t12 ) => ( t21 ) => ( t31 | t32 )
+//
+// Task t12 has no undo.
+//
+// Final task statuses are tested based on the resulting events list.
+//
+var sequenceTests = []struct{ setup, result string }{{
+ setup: "",
+ result: "t11:do t12:do t21:do t31:do t32:do",
+}, {
+ setup: "t11:was-done t12:was-doing",
+ result: "t12:do t21:do t31:do t32:do",
+}, {
+ setup: "t11:was-done t12:was-doing chg:abort",
+ result: "t11:undo",
+}, {
+ setup: "t12:do-retry",
+ result: "t11:do t12:do t12:do-retry t12:do t21:do t31:do t32:do",
+}, {
+ setup: "t11:do-block t12:do-error",
+ result: "t11:do t11:do-block t12:do t12:do-error t11:do-unblock t11:undo",
+}, {
+ setup: "t11:do-error t12:do-block",
+ result: "t11:do t11:do-error t12:do t12:do-block t12:do-unblock",
+}, {
+ setup: "t11:do-block t11:do-retry t12:do-error",
+ result: "t11:do t11:do-block t12:do t12:do-error t11:do-unblock t11:do-retry t11:undo",
+}, {
+ setup: "t11:do-error t12:do-block t12:do-retry",
+ result: "t11:do t11:do-error t12:do t12:do-block t12:do-unblock t12:do-retry",
+}, {
+ setup: "t31:do-error t21:undo-error",
+ result: "t11:do t12:do t21:do t31:do t31:do-error t32:do t32:undo t21:undo t21:undo-error t11:undo",
+}, {
+ setup: "t21:do-set-ready",
+ result: "t11:do t12:do t21:do t31:do t32:do",
+}, {
+ setup: "t31:do-error t21:undo-set-ready",
+ result: "t11:do t12:do t21:do t31:do t31:do-error t32:do t32:undo t21:undo t11:undo",
+}, {
+ setup: "t11:was-done:1 t12:was-done:2 t21:was-done:1,2 t31:was-done:1 t32:do-error:2",
+ result: "t31:undo t32:do t32:do-error t21:undo t11:undo",
+}, {
+ setup: "t11:was-done:1 t12:was-done:2 t21:was-done:2 t31:was-done:2 t32:do-error:2",
+ result: "t31:undo t32:do t32:do-error t21:undo",
+}}
+
+func (ts *taskRunnerSuite) TestSequenceTests(c *C) {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch := make(chan string, 256)
+ fn := func(label string) state.HandlerFunc {
+ return func(task *state.Task, tomb *tomb.Tomb) error {
+ st.Lock()
+ defer st.Unlock()
+ ch <- task.Summary() + ":" + label
+ var isSet bool
+ if task.Get(label+"-block", &isSet) == nil && isSet {
+ ch <- task.Summary() + ":" + label + "-block"
+ st.Unlock()
+ <-tomb.Dying()
+ st.Lock()
+ ch <- task.Summary() + ":" + label + "-unblock"
+ }
+ if task.Get(label+"-retry", &isSet) == nil && isSet {
+ task.Set(label+"-retry", false)
+ ch <- task.Summary() + ":" + label + "-retry"
+ return &state.Retry{}
+ }
+ if task.Get(label+"-error", &isSet) == nil && isSet {
+ ch <- task.Summary() + ":" + label + "-error"
+ return errors.New("boom")
+ }
+ if task.Get(label+"-set-ready", &isSet) == nil && isSet {
+ switch task.Status() {
+ case state.DoingStatus:
+ task.SetStatus(state.DoneStatus)
+ case state.UndoingStatus:
+ task.SetStatus(state.UndoneStatus)
+ }
+ }
+ return nil
+ }
+ }
+ r.AddHandler("do", fn("do"), nil)
+ r.AddHandler("do-undo", fn("do"), fn("undo"))
+
+ for _, test := range sequenceTests {
+ st.Lock()
+
+ // Delete previous changes.
+ st.Prune(1, 1)
+
+ chg := st.NewChange("install", "...")
+ tasks := make(map[string]*state.Task)
+ for _, name := range strings.Fields("t11 t12 t21 t31 t32") {
+ if name == "t12" {
+ tasks[name] = st.NewTask("do", name)
+ } else {
+ tasks[name] = st.NewTask("do-undo", name)
+ }
+ chg.AddTask(tasks[name])
+ }
+ tasks["t21"].WaitFor(tasks["t11"])
+ tasks["t21"].WaitFor(tasks["t12"])
+ tasks["t31"].WaitFor(tasks["t21"])
+ tasks["t32"].WaitFor(tasks["t21"])
+ st.Unlock()
+
+ c.Logf("-----")
+ c.Logf("Testing setup: %s", test.setup)
+
+ statuses := make(map[string]state.Status)
+ for s := state.DefaultStatus; s <= state.ErrorStatus; s++ {
+ statuses[strings.ToLower(s.String())] = s
+ }
+
+ // Reset and prepare initial task state.
+ st.Lock()
+ for _, t := range chg.Tasks() {
+ t.SetStatus(state.DefaultStatus)
+ t.Set("do-error", false)
+ t.Set("do-block", false)
+ t.Set("undo-error", false)
+ t.Set("undo-block", false)
+ }
+ for _, item := range strings.Fields(test.setup) {
+ parts := strings.Split(item, ":")
+ if parts[0] == "chg" && parts[1] == "abort" {
+ chg.Abort()
+ } else {
+ if strings.HasPrefix(parts[1], "was-") {
+ tasks[parts[0]].SetStatus(statuses[parts[1][4:]])
+ } else {
+ tasks[parts[0]].Set(parts[1], true)
+ }
+ }
+ if len(parts) > 2 {
+ lanes := strings.Split(parts[2], ",")
+ for _, lane := range lanes {
+ n, err := strconv.Atoi(lane)
+ c.Assert(err, IsNil)
+ tasks[parts[0]].JoinLane(n)
+ }
+ }
+ }
+ st.Unlock()
+
+ // Run change until final.
+ ensureChange(c, r, sb, chg)
+
+ // Compute order of events observed.
+ var events []string
+ var done bool
+ for !done {
+ select {
+ case ev := <-ch:
+ events = append(events, ev)
+ // Make t11/t12 and t31/t32 always show up in the
+ // same order if they're next to each other.
+ for i := len(events) - 2; i >= 0; i-- {
+ prev := events[i]
+ next := events[i+1]
+ switch strings.Split(next, ":")[1] {
+ case "do-unblock", "undo-unblock":
+ default:
+ if prev[1] == next[1] && prev[2] > next[2] {
+ events[i], events[i+1] = next, prev
+ continue
+ }
+ }
+ break
+ }
+ default:
+ done = true
+ }
+ }
+
+ c.Logf("Expected result: %s", test.result)
+ c.Assert(strings.Join(events, " "), Equals, test.result, Commentf("setup: %s", test.setup))
+
+ // Compute final expected status for tasks.
+ finalStatus := make(map[string]state.Status)
+ // ... default when no handler is called
+ for tname := range tasks {
+ finalStatus[tname] = state.HoldStatus
+ }
+ // ... overwrite based on relevant setup
+ for _, item := range strings.Fields(test.setup) {
+ parts := strings.Split(item, ":")
+ if parts[0] == "chg" && parts[1] == "abort" && strings.Contains(test.setup, "t12:was-doing") {
+ // t12 has no undo so must hold if asked to abort when was doing.
+ finalStatus["t12"] = state.HoldStatus
+ }
+ if !strings.HasPrefix(parts[1], "was-") {
+ continue
+ }
+ switch strings.TrimPrefix(parts[1], "was-") {
+ case "do", "doing", "done":
+ finalStatus[parts[0]] = state.DoneStatus
+ case "abort", "undo", "undoing", "undone":
+ if parts[0] == "t12" {
+ finalStatus[parts[0]] = state.DoneStatus // no undo for t12
+ } else {
+ finalStatus[parts[0]] = state.UndoneStatus
+ }
+ case "was-error":
+ finalStatus[parts[0]] = state.ErrorStatus
+ case "was-hold":
+ finalStatus[parts[0]] = state.ErrorStatus
+ }
+ }
+ // ... and overwrite based on events observed.
+ for _, ev := range events {
+ parts := strings.Split(ev, ":")
+ switch parts[1] {
+ case "do":
+ finalStatus[parts[0]] = state.DoneStatus
+ case "undo":
+ finalStatus[parts[0]] = state.UndoneStatus
+ case "do-error", "undo-error":
+ finalStatus[parts[0]] = state.ErrorStatus
+ case "do-retry":
+ if parts[0] == "t12" && finalStatus["t11"] == state.ErrorStatus {
+ // t12 has no undo so must hold if asked to abort on retry.
+ finalStatus["t12"] = state.HoldStatus
+ }
+ }
+ }
+
+ st.Lock()
+ var gotStatus, wantStatus []string
+ for _, task := range chg.Tasks() {
+ gotStatus = append(gotStatus, task.Summary()+":"+task.Status().String())
+ wantStatus = append(wantStatus, task.Summary()+":"+finalStatus[task.Summary()].String())
+ }
+ st.Unlock()
+
+ c.Logf("Expected statuses: %s", strings.Join(wantStatus, " "))
+ comment := Commentf("calls: %s", test.result)
+ c.Assert(strings.Join(gotStatus, " "), Equals, strings.Join(wantStatus, " "), comment)
+ }
+}
+
+func (ts *taskRunnerSuite) TestExternalAbort(c *C) {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch := make(chan bool)
+ r.AddHandler("blocking", func(t *state.Task, tb *tomb.Tomb) error {
+ ch <- true
+ <-tb.Dying()
+ return nil
+ }, nil)
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t := st.NewTask("blocking", "...")
+ chg.AddTask(t)
+ st.Unlock()
+
+ r.Ensure()
+ <-ch
+
+ st.Lock()
+ chg.Abort()
+ st.Unlock()
+
+ // The Abort above must make Ensure kill the task, or this will never end.
+ ensureChange(c, r, sb, chg)
+}
+
+func (ts *taskRunnerSuite) TestStopHandlerJustFinishing(c *C) {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch := make(chan bool)
+ r.AddHandler("just-finish", func(t *state.Task, tb *tomb.Tomb) error {
+ ch <- true
+ <-tb.Dying()
+ // just ignore and actually finishes
+ return nil
+ }, nil)
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t := st.NewTask("just-finish", "...")
+ chg.AddTask(t)
+ st.Unlock()
+
+ r.Ensure()
+ <-ch
+ r.Stop()
+
+ st.Lock()
+ defer st.Unlock()
+ c.Check(t.Status(), Equals, state.DoneStatus)
+}
+
+func (ts *taskRunnerSuite) TestStopAskForRetry(c *C) {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch := make(chan bool)
+ r.AddHandler("ask-for-retry", func(t *state.Task, tb *tomb.Tomb) error {
+ ch <- true
+ <-tb.Dying()
+ // ask for retry
+ return &state.Retry{}
+ }, nil)
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t := st.NewTask("ask-for-retry", "...")
+ chg.AddTask(t)
+ st.Unlock()
+
+ r.Ensure()
+ <-ch
+ r.Stop()
+
+ st.Lock()
+ defer st.Unlock()
+ c.Check(t.Status(), Equals, state.DoingStatus)
+}
+
+func (ts *taskRunnerSuite) TestRetryAfterDuration(c *C) {
+ ensureBeforeTick := make(chan bool, 1)
+ sb := &stateBackend{
+ ensureBefore: time.Hour,
+ ensureBeforeSeen: ensureBeforeTick,
+ }
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch := make(chan bool)
+ ask := 0
+ r.AddHandler("ask-for-retry", func(t *state.Task, _ *tomb.Tomb) error {
+ ask++
+ if ask == 1 {
+ return &state.Retry{After: time.Minute}
+ }
+ ch <- true
+ return nil
+ }, nil)
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t := st.NewTask("ask-for-retry", "...")
+ chg.AddTask(t)
+ st.Unlock()
+
+ tock := time.Now()
+ restore := state.MockTime(tock)
+ defer restore()
+ r.Ensure() // will run and be rescheduled in a minute
+ select {
+ case <-ensureBeforeTick:
+ case <-time.After(2 * time.Second):
+ c.Fatal("EnsureBefore wasn't called")
+ }
+
+ st.Lock()
+ defer st.Unlock()
+ c.Check(t.Status(), Equals, state.DoingStatus)
+
+ c.Check(ask, Equals, 1)
+ c.Check(sb.ensureBefore, Equals, 1*time.Minute)
+ schedule := t.AtTime()
+ c.Check(schedule.IsZero(), Equals, false)
+
+ state.MockTime(tock.Add(5 * time.Second))
+ sb.ensureBefore = time.Hour
+ st.Unlock()
+ r.Ensure() // too soon
+ st.Lock()
+
+ c.Check(t.Status(), Equals, state.DoingStatus)
+ c.Check(ask, Equals, 1)
+ c.Check(sb.ensureBefore, Equals, 55*time.Second)
+ c.Check(t.AtTime().Equal(schedule), Equals, true)
+
+ state.MockTime(schedule)
+ sb.ensureBefore = time.Hour
+ st.Unlock()
+ r.Ensure() // time to run again
+ select {
+ case <-ch:
+ case <-time.After(2 * time.Second):
+ c.Fatal("handler wasn't called")
+ }
+
+ // wait for handler to finish
+ r.Wait()
+
+ st.Lock()
+ c.Check(t.Status(), Equals, state.DoneStatus)
+ c.Check(ask, Equals, 2)
+ c.Check(sb.ensureBefore, Equals, time.Hour)
+ c.Check(t.AtTime().IsZero(), Equals, true)
+}
+
+func (ts *taskRunnerSuite) TestTaskSerialization(c *C) {
+ ensureBeforeTick := make(chan bool, 1)
+ sb := &stateBackend{
+ ensureBefore: time.Hour,
+ ensureBeforeSeen: ensureBeforeTick,
+ }
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch1 := make(chan bool)
+ ch2 := make(chan bool)
+ r.AddHandler("do1", func(t *state.Task, _ *tomb.Tomb) error {
+ ch1 <- true
+ ch1 <- true
+ return nil
+ }, nil)
+ r.AddHandler("do2", func(t *state.Task, _ *tomb.Tomb) error {
+ ch2 <- true
+ return nil
+ }, nil)
+
+ // start first do1, and then do2 when nothing else is running
+ startedDo1 := false
+ r.SetBlocked(func(t *state.Task, running []*state.Task) bool {
+ if t.Kind() == "do2" && (len(running) != 0 || !startedDo1) {
+ return true
+ }
+ if t.Kind() == "do1" {
+ startedDo1 = true
+ }
+ return false
+ })
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t1 := st.NewTask("do1", "...")
+ chg.AddTask(t1)
+ t2 := st.NewTask("do2", "...")
+ chg.AddTask(t2)
+ st.Unlock()
+
+ r.Ensure() // will start only one, do1
+
+ select {
+ case <-ch1:
+ case <-time.After(2 * time.Second):
+ c.Fatal("do1 wasn't called")
+ }
+
+ c.Check(ensureBeforeTick, HasLen, 0)
+ c.Check(ch2, HasLen, 0)
+
+ r.Ensure() // won't yet start anything new
+
+ c.Check(ensureBeforeTick, HasLen, 0)
+ c.Check(ch2, HasLen, 0)
+
+ // finish do1
+ select {
+ case <-ch1:
+ case <-time.After(2 * time.Second):
+ c.Fatal("do1 wasn't continued")
+ }
+
+ // getting an EnsureBefore 0 call
+ select {
+ case <-ensureBeforeTick:
+ case <-time.After(2 * time.Second):
+ c.Fatal("EnsureBefore wasn't called")
+ }
+ c.Check(sb.ensureBefore, Equals, time.Duration(0))
+
+ r.Ensure() // will start do2
+
+ select {
+ case <-ch2:
+ case <-time.After(2 * time.Second):
+ c.Fatal("do2 wasn't called")
+ }
+
+ // no more EnsureBefore calls
+ c.Check(ensureBeforeTick, HasLen, 0)
+}
+
+func (ts *taskRunnerSuite) TestPrematureChangeReady(c *C) {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ ch := make(chan bool)
+ r.AddHandler("block-undo", func(t *state.Task, tb *tomb.Tomb) error { return nil },
+ func(t *state.Task, tb *tomb.Tomb) error {
+ ch <- true
+ <-ch
+ return nil
+ })
+ r.AddHandler("fail", func(t *state.Task, tb *tomb.Tomb) error {
+ return errors.New("BAM")
+ }, nil)
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t1 := st.NewTask("block-undo", "...")
+ t2 := st.NewTask("fail", "...")
+ chg.AddTask(t1)
+ chg.AddTask(t2)
+ st.Unlock()
+
+ r.Ensure() // Error
+ r.Wait()
+ r.Ensure() // Block on undo
+ <-ch
+
+ defer func() {
+ ch <- true
+ r.Wait()
+ }()
+
+ st.Lock()
+ defer st.Unlock()
+
+ if chg.IsReady() || chg.Status().Ready() {
+ c.Errorf("Change considered ready prematurely")
+ }
+
+ c.Assert(chg.Err(), IsNil)
+}
+
+func (ts *taskRunnerSuite) TestCleanup(c *C) {
+ sb := &stateBackend{}
+ st := state.New(sb)
+ r := state.NewTaskRunner(st)
+ defer r.Stop()
+
+ r.AddHandler("clean-it", func(t *state.Task, tb *tomb.Tomb) error { return nil }, nil)
+ r.AddHandler("other", func(t *state.Task, tb *tomb.Tomb) error { return nil }, nil)
+
+ called := 0
+ r.AddCleanup("clean-it", func(t *state.Task, tb *tomb.Tomb) error {
+ called++
+ if called == 1 {
+ return fmt.Errorf("retry me")
+ }
+ return nil
+ })
+
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ t1 := st.NewTask("clean-it", "...")
+ t2 := st.NewTask("other", "...")
+ chg.AddTask(t1)
+ chg.AddTask(t2)
+ st.Unlock()
+
+ chgIsClean := func() bool {
+ st.Lock()
+ defer st.Unlock()
+ return chg.IsClean()
+ }
+
+ // Mark tasks as done.
+ ensureChange(c, r, sb, chg)
+
+ // First time it errors, then it works, then it's ignored.
+ c.Assert(chgIsClean(), Equals, false)
+ c.Assert(called, Equals, 0)
+ r.Ensure()
+ r.Wait()
+ c.Assert(chgIsClean(), Equals, false)
+ c.Assert(called, Equals, 1)
+ r.Ensure()
+ r.Wait()
+ c.Assert(chgIsClean(), Equals, true)
+ c.Assert(called, Equals, 2)
+ r.Ensure()
+ r.Wait()
+ c.Assert(chgIsClean(), Equals, true)
+ c.Assert(called, Equals, 2)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package overlord
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/snapcore/snapd/logger"
+
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// StateManager is implemented by types responsible for observing
+// the system and manipulating it to reflect the desired state.
+type StateManager interface {
+ // Ensure forces a complete evaluation of the current state.
+ // See StateEngine.Ensure for more details.
+ Ensure() error
+
+ // Wait asks manager to wait for all running activities to finish.
+ Wait()
+
+ // Stop asks the manager to terminate all activities running concurrently.
+ // It must not return before these activities are finished.
+ Stop()
+}
+
+// StateEngine controls the dispatching of state changes to state managers.
+//
+// Most of the actual work performed by the state engine is in fact done
+// by the individual managers registered. These managers must be able to
+// cope with Ensure calls in any order, coordinating among themselves
+// solely via the state.
+type StateEngine struct {
+ state *state.State
+ stopped bool
+ // managers in use
+ mgrLock sync.Mutex
+ managers []StateManager
+}
+
+// NewStateEngine returns a new state engine.
+func NewStateEngine(s *state.State) *StateEngine {
+ return &StateEngine{
+ state: s,
+ }
+}
+
+// State returns the current system state.
+func (se *StateEngine) State() *state.State {
+ return se.state
+}
+
+type ensureError struct {
+ errs []error
+}
+
+func (e *ensureError) Error() string {
+ return fmt.Sprintf("state ensure errors: %v", e.errs)
+}
+
+// Ensure asks every manager to ensure that they are doing the necessary
+// work to put the current desired system state in place by calling their
+// respective Ensure methods.
+//
+// Managers must evaluate the desired state completely when they receive
+// that request, and report whether they found any critical issues. They
+// must not perform long running activities during that operation, though.
+// These should be performed in properly tracked changes and tasks.
+func (se *StateEngine) Ensure() error {
+ se.mgrLock.Lock()
+ defer se.mgrLock.Unlock()
+ if se.stopped {
+ return fmt.Errorf("state engine already stopped")
+ }
+ var errs []error
+ for _, m := range se.managers {
+ err := m.Ensure()
+ if err != nil {
+ logger.Noticef("state ensure error: %v", err)
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) != 0 {
+ return &ensureError{errs}
+ }
+ return nil
+}
+
+// AddManager adds the provided manager to take part in state operations.
+func (se *StateEngine) AddManager(m StateManager) {
+ se.mgrLock.Lock()
+ defer se.mgrLock.Unlock()
+ se.managers = append(se.managers, m)
+}
+
+// Wait waits for all managers current activities.
+func (se *StateEngine) Wait() {
+ se.mgrLock.Lock()
+ defer se.mgrLock.Unlock()
+ if se.stopped {
+ return
+ }
+ for _, m := range se.managers {
+ m.Wait()
+ }
+}
+
+// Stop asks all managers to terminate activities running concurrently.
+func (se *StateEngine) Stop() {
+ se.mgrLock.Lock()
+ defer se.mgrLock.Unlock()
+ if se.stopped {
+ return
+ }
+ for _, m := range se.managers {
+ m.Stop()
+ }
+ se.stopped = true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package overlord_test
+
+import (
+ "errors"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type stateEngineSuite struct{}
+
+var _ = Suite(&stateEngineSuite{})
+
+func (ses *stateEngineSuite) TestNewAndState(c *C) {
+ s := state.New(nil)
+ se := overlord.NewStateEngine(s)
+
+ c.Check(se.State(), Equals, s)
+}
+
+type fakeManager struct {
+ name string
+ calls *[]string
+ ensureError, stopError error
+}
+
+func (fm *fakeManager) Ensure() error {
+ *fm.calls = append(*fm.calls, "ensure:"+fm.name)
+ return fm.ensureError
+}
+
+func (fm *fakeManager) Stop() {
+ *fm.calls = append(*fm.calls, "stop:"+fm.name)
+}
+
+func (fm *fakeManager) Wait() {
+ *fm.calls = append(*fm.calls, "wait:"+fm.name)
+}
+
+var _ overlord.StateManager = (*fakeManager)(nil)
+
+func (ses *stateEngineSuite) TestEnsure(c *C) {
+ s := state.New(nil)
+ se := overlord.NewStateEngine(s)
+
+ calls := []string{}
+
+ mgr1 := &fakeManager{name: "mgr1", calls: &calls}
+ mgr2 := &fakeManager{name: "mgr2", calls: &calls}
+
+ se.AddManager(mgr1)
+ se.AddManager(mgr2)
+
+ err := se.Ensure()
+ c.Assert(err, IsNil)
+ c.Check(calls, DeepEquals, []string{"ensure:mgr1", "ensure:mgr2"})
+
+ err = se.Ensure()
+ c.Assert(err, IsNil)
+ c.Check(calls, DeepEquals, []string{"ensure:mgr1", "ensure:mgr2", "ensure:mgr1", "ensure:mgr2"})
+}
+
+func (ses *stateEngineSuite) TestEnsureError(c *C) {
+ s := state.New(nil)
+ se := overlord.NewStateEngine(s)
+
+ calls := []string{}
+
+ err1 := errors.New("boom1")
+ err2 := errors.New("boom2")
+
+ mgr1 := &fakeManager{name: "mgr1", calls: &calls, ensureError: err1}
+ mgr2 := &fakeManager{name: "mgr2", calls: &calls, ensureError: err2}
+
+ se.AddManager(mgr1)
+ se.AddManager(mgr2)
+
+ err := se.Ensure()
+ c.Check(err.Error(), DeepEquals, "state ensure errors: [boom1 boom2]")
+ c.Check(calls, DeepEquals, []string{"ensure:mgr1", "ensure:mgr2"})
+}
+
+func (ses *stateEngineSuite) TestStop(c *C) {
+ s := state.New(nil)
+ se := overlord.NewStateEngine(s)
+
+ calls := []string{}
+
+ mgr1 := &fakeManager{name: "mgr1", calls: &calls}
+ mgr2 := &fakeManager{name: "mgr2", calls: &calls}
+
+ se.AddManager(mgr1)
+ se.AddManager(mgr2)
+
+ se.Stop()
+ c.Check(calls, DeepEquals, []string{"stop:mgr1", "stop:mgr2"})
+ se.Stop()
+ c.Check(calls, HasLen, 2)
+
+ err := se.Ensure()
+ c.Check(err, ErrorMatches, "state engine already stopped")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+const (
+ // bootloader variable used to determine if boot was successful.
+ // Set to value of either bootloaderBootmodeTry (when attempting
+ // to boot a new rootfs) or bootloaderBootmodeSuccess (to denote
+ // that the boot of the new rootfs was successful).
+ bootmodeVar = "snap_mode"
+
+ // Initial and final values
+ modeTry = "try"
+ modeSuccess = ""
+)
+
+var (
+ // ErrBootloader is returned if the bootloader can not be determined
+ ErrBootloader = errors.New("cannot determine bootloader")
+)
+
+// Bootloader provides an interface to interact with the system
+// bootloader
+type Bootloader interface {
+ // Return the value of the specified bootloader variable
+ GetBootVars(names ...string) (map[string]string, error)
+
+ // Set the value of the specified bootloader variable
+ SetBootVars(values map[string]string) error
+
+ // Dir returns the bootloader directory
+ Dir() string
+
+ // Name returns the bootloader name
+ Name() string
+
+ // ConfigFile returns the name of the config file
+ ConfigFile() string
+}
+
+// InstallBootConfig installs the bootloader config from the gadget
+// snap dir into the right place.
+func InstallBootConfig(gadgetDir string) error {
+ for _, bl := range []Bootloader{&grub{}, &uboot{}} {
+ // the bootloader config file has to be root of the gadget snap
+ gadgetFile := filepath.Join(gadgetDir, bl.Name()+".conf")
+ if !osutil.FileExists(gadgetFile) {
+ continue
+ }
+
+ systemFile := bl.ConfigFile()
+ if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil {
+ return err
+ }
+ return osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite)
+ }
+
+ return fmt.Errorf("cannot find boot config in %q", gadgetDir)
+}
+
+var forcedBootloader Bootloader
+
+// FindBootloader returns the bootloader for the given system
+// or an error if no bootloader is found
+func FindBootloader() (Bootloader, error) {
+ if forcedBootloader != nil {
+ return forcedBootloader, nil
+ }
+
+ // try uboot
+ if uboot := newUboot(); uboot != nil {
+ return uboot, nil
+ }
+
+ // no, try grub
+ if grub := newGrub(); grub != nil {
+ return grub, nil
+ }
+
+ // no, weeeee
+ return nil, ErrBootloader
+}
+
+// ForceBootloader can be used to force setting a booloader to that FindBootloader will not use the usual lookup process, use nil to reset to normal lookup.
+func ForceBootloader(booloader Bootloader) {
+ forcedBootloader = booloader
+}
+
+// MarkBootSuccessful marks the current boot as successful. This means
+// that snappy will consider this combination of kernel/os a valid
+// target for rollback
+func MarkBootSuccessful(bootloader Bootloader) error {
+ m, err := bootloader.GetBootVars("snap_mode", "snap_try_core", "snap_try_kernel")
+ if err != nil {
+ return err
+ }
+
+ // snap_mode goes from "" -> "try" -> "trying" -> ""
+ // so if we are not in "trying" mode, nothing to do here
+ if m["snap_mode"] != "trying" {
+ return nil
+ }
+
+ // update the boot vars
+ for _, k := range []string{"kernel", "core"} {
+ tryBootVar := fmt.Sprintf("snap_try_%s", k)
+ bootVar := fmt.Sprintf("snap_%s", k)
+ // update the boot vars
+ if m[tryBootVar] != "" {
+ m[bootVar] = m[tryBootVar]
+ m[tryBootVar] = ""
+ }
+ }
+ m["snap_mode"] = modeSuccess
+
+ return bootloader.SetBootVars(m)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+// partition specific testsuite
+type PartitionTestSuite struct {
+}
+
+var _ = Suite(&PartitionTestSuite{})
+
+type mockBootloader struct {
+ bootVars map[string]string
+}
+
+func newMockBootloader() *mockBootloader {
+ return &mockBootloader{
+ bootVars: make(map[string]string),
+ }
+}
+func (b *mockBootloader) Name() string {
+ return "mocky"
+}
+func (b *mockBootloader) Dir() string {
+ return "/boot/mocky"
+}
+func (b *mockBootloader) GetBootVars(names ...string) (map[string]string, error) {
+ out := map[string]string{}
+ for _, name := range names {
+ out[name] = b.bootVars[name]
+ }
+
+ return out, nil
+}
+func (b *mockBootloader) SetBootVars(values map[string]string) error {
+ for k, v := range values {
+ b.bootVars[k] = v
+ }
+ return nil
+}
+func (b *mockBootloader) ConfigFile() string {
+ return "/boot/mocky/mocky.env"
+}
+
+func (s *PartitionTestSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll((&grub{}).Dir(), 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll((&uboot{}).Dir(), 0755)
+ c.Assert(err, IsNil)
+}
+
+func (s *PartitionTestSuite) TestForceBootloader(c *C) {
+ b := newMockBootloader()
+ ForceBootloader(b)
+ defer ForceBootloader(nil)
+
+ got, err := FindBootloader()
+ c.Assert(err, IsNil)
+ c.Check(got, Equals, b)
+}
+
+func (s *PartitionTestSuite) TestMarkBootSuccessfulAllSnap(c *C) {
+ b := newMockBootloader()
+ b.bootVars["snap_mode"] = "trying"
+ b.bootVars["snap_try_core"] = "os1"
+ b.bootVars["snap_try_kernel"] = "k1"
+ err := MarkBootSuccessful(b)
+ c.Assert(err, IsNil)
+
+ expected := map[string]string{
+ // cleared
+ "snap_mode": "",
+ "snap_try_kernel": "",
+ "snap_try_core": "",
+ // updated
+ "snap_kernel": "k1",
+ "snap_core": "os1",
+ }
+ c.Assert(b.bootVars, DeepEquals, expected)
+
+ // do it again, verify its still valid
+ err = MarkBootSuccessful(b)
+ c.Assert(err, IsNil)
+ c.Assert(b.bootVars, DeepEquals, expected)
+}
+
+func (s *PartitionTestSuite) TestMarkBootSuccessfulKKernelUpdate(c *C) {
+ b := newMockBootloader()
+ b.bootVars["snap_mode"] = "trying"
+ b.bootVars["snap_core"] = "os1"
+ b.bootVars["snap_kernel"] = "k1"
+ b.bootVars["snap_try_core"] = ""
+ b.bootVars["snap_try_kernel"] = "k2"
+ err := MarkBootSuccessful(b)
+ c.Assert(err, IsNil)
+ c.Assert(b.bootVars, DeepEquals, map[string]string{
+ // cleared
+ "snap_mode": "",
+ "snap_try_kernel": "",
+ "snap_try_core": "",
+ // unchanged
+ "snap_core": "os1",
+ // updated
+ "snap_kernel": "k2",
+ })
+}
+
+func (s *PartitionTestSuite) TestInstallBootloaderConfigNoConfig(c *C) {
+ err := InstallBootConfig(c.MkDir())
+ c.Assert(err, ErrorMatches, `cannot find boot config in.*`)
+}
+
+func (s *PartitionTestSuite) TestInstallBootloaderConfig(c *C) {
+ for _, t := range []struct{ gadgetFile, systemFile string }{
+ {"grub.conf", "/boot/grub/grub.cfg"},
+ {"uboot.conf", "/boot/uboot/uboot.env"},
+ } {
+ mockGadgetDir := c.MkDir()
+ err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), nil, 0644)
+ c.Assert(err, IsNil)
+ err = InstallBootConfig(mockGadgetDir)
+ c.Assert(err, IsNil)
+ fn := filepath.Join(dirs.GlobalRootDir, t.systemFile)
+ c.Assert(osutil.FileExists(fn), Equals, true)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+
+ "github.com/mvo5/goconfigparser"
+)
+
+// var to make it testable
+var (
+ grubEnvCmd = "/usr/bin/grub-editenv"
+)
+
+type grub struct {
+}
+
+// newGrub create a new Grub bootloader object
+func newGrub() Bootloader {
+ g := &grub{}
+ if !osutil.FileExists(g.ConfigFile()) {
+ return nil
+ }
+
+ return g
+}
+
+func (g *grub) Name() string {
+ return "grub"
+}
+
+func (g *grub) Dir() string {
+ return filepath.Join(dirs.GlobalRootDir, "/boot/grub")
+}
+
+func (g *grub) ConfigFile() string {
+ return filepath.Join(g.Dir(), "grub.cfg")
+}
+
+func (g *grub) envFile() string {
+ return filepath.Join(g.Dir(), "grubenv")
+}
+
+func (g *grub) GetBootVars(names ...string) (map[string]string, error) {
+ out := map[string]string{}
+
+ // Grub doesn't provide a get verb, so retrieve all values and
+ // search for the required variable ourselves.
+ output, err := runCommand(grubEnvCmd, g.envFile(), "list")
+ if err != nil {
+ return nil, err
+ }
+
+ cfg := goconfigparser.New()
+ cfg.AllowNoSectionHeader = true
+ if err := cfg.ReadString(output); err != nil {
+ return nil, err
+ }
+
+ for _, name := range names {
+ v, err := cfg.Get("", name)
+ if err != nil {
+ return nil, err
+ }
+ out[name] = v
+ }
+
+ return out, nil
+}
+
+func (g *grub) SetBootVars(values map[string]string) error {
+ // note that strings are not quoted since because
+ // runCommand does not use a shell and thus adding quotes
+ // stores them in the environment file (which is not desirable)
+ args := []string{grubEnvCmd, g.envFile(), "set"}
+ for k, v := range values {
+ args = append(args, fmt.Sprintf("%s=%s", k, v))
+ }
+ _, err := runCommand(args...)
+ return err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "sort"
+
+ "github.com/snapcore/snapd/dirs"
+
+ . "gopkg.in/check.v1"
+)
+
+func mockGrubEditenvList(cmd ...string) (string, error) {
+ mockGrubEditenvOutput := fmt.Sprintf("%s=regular", bootmodeVar)
+ return mockGrubEditenvOutput, nil
+}
+
+func mockGrubFile(c *C, newPath string, mode os.FileMode) {
+ err := ioutil.WriteFile(newPath, []byte(""), mode)
+ c.Assert(err, IsNil)
+}
+
+func (s *PartitionTestSuite) makeFakeGrubEnv(c *C) {
+ // these files just needs to exist
+ g := &grub{}
+ mockGrubFile(c, g.ConfigFile(), 0644)
+ mockGrubFile(c, g.envFile(), 0644)
+}
+
+func (s *PartitionTestSuite) TestNewGrubNoGrubReturnsNil(c *C) {
+ dirs.GlobalRootDir = "/something/not/there"
+
+ g := newGrub()
+ c.Assert(g, IsNil)
+}
+
+func (s *PartitionTestSuite) TestNewGrub(c *C) {
+ s.makeFakeGrubEnv(c)
+
+ g := newGrub()
+ c.Assert(g, NotNil)
+ c.Assert(g, FitsTypeOf, &grub{})
+}
+
+func (s *PartitionTestSuite) TestGetBootloaderWithGrub(c *C) {
+ s.makeFakeGrubEnv(c)
+
+ bootloader, err := FindBootloader()
+ c.Assert(err, IsNil)
+ c.Assert(bootloader, FitsTypeOf, &grub{})
+}
+
+func (s *PartitionTestSuite) TestGetBootVer(c *C) {
+ s.makeFakeGrubEnv(c)
+ runCommand = mockGrubEditenvList
+
+ g := newGrub()
+ v, err := g.GetBootVars(bootmodeVar)
+ c.Assert(err, IsNil)
+ c.Check(v, HasLen, 1)
+ c.Check(v[bootmodeVar], Equals, "regular")
+}
+
+func (s *PartitionTestSuite) TestSetBootVer(c *C) {
+ s.makeFakeGrubEnv(c)
+ cmds := [][]string{}
+ runCommand = func(cmd ...string) (string, error) {
+ cmds = append(cmds, cmd)
+ return "", nil
+ }
+
+ g := newGrub()
+ err := g.SetBootVars(map[string]string{
+ "k1": "v1",
+ "k2": "v2",
+ })
+ c.Assert(err, IsNil)
+ c.Check(cmds, HasLen, 1)
+ c.Check(cmds[0][0:3], DeepEquals, []string{
+ "/usr/bin/grub-editenv", g.(*grub).envFile(), "set",
+ })
+ // need to sort, its coming from a slice
+ kwargs := cmds[0][3:]
+ sort.Strings(kwargs)
+ c.Check(kwargs, DeepEquals, []string{"k1=v1", "k2=v2"})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "path/filepath"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+
+ "github.com/mvo5/uboot-go/uenv"
+)
+
+type uboot struct {
+}
+
+// newUboot create a new Uboot bootloader object
+func newUboot() Bootloader {
+ u := &uboot{}
+ if !osutil.FileExists(u.envFile()) {
+ return nil
+ }
+
+ return u
+}
+
+func (u *uboot) Name() string {
+ return "uboot"
+}
+
+func (u *uboot) Dir() string {
+ return filepath.Join(dirs.GlobalRootDir, "/boot/uboot")
+}
+
+func (u *uboot) ConfigFile() string {
+ return u.envFile()
+}
+
+func (u *uboot) envFile() string {
+ return filepath.Join(u.Dir(), "uboot.env")
+}
+
+func (u *uboot) SetBootVars(values map[string]string) error {
+ env, err := uenv.Open(u.envFile())
+ if err != nil {
+ return err
+ }
+
+ dirty := false
+ for k, v := range values {
+ // already set to the right value, nothing to do
+ if env.Get(k) == v {
+ continue
+ }
+ env.Set(k, v)
+ dirty = true
+ }
+
+ if dirty {
+ return env.Save()
+ }
+
+ return nil
+}
+
+func (u *uboot) GetBootVars(names ...string) (map[string]string, error) {
+ out := map[string]string{}
+
+ env, err := uenv.Open(u.envFile())
+ if err != nil {
+ return nil, err
+ }
+
+ for _, name := range names {
+ out[name] = env.Get(name)
+ }
+
+ return out, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "os"
+ "time"
+
+ "github.com/mvo5/uboot-go/uenv"
+
+ . "gopkg.in/check.v1"
+)
+
+func (s *PartitionTestSuite) makeFakeUbootEnv(c *C) {
+ u := &uboot{}
+
+ // ensure that we have a valid uboot.env too
+ env, err := uenv.Create(u.envFile(), 4096)
+ c.Assert(err, IsNil)
+ err = env.Save()
+ c.Assert(err, IsNil)
+}
+
+func (s *PartitionTestSuite) TestNewUbootNoUbootReturnsNil(c *C) {
+ u := newUboot()
+ c.Assert(u, IsNil)
+}
+
+func (s *PartitionTestSuite) TestNewUboot(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ c.Assert(u, NotNil)
+ c.Assert(u, FitsTypeOf, &uboot{})
+}
+
+func (s *PartitionTestSuite) TestUbootGetEnvVar(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ c.Assert(u, NotNil)
+ err := u.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "4",
+ })
+ c.Assert(err, IsNil)
+
+ m, err := u.GetBootVars("snap_mode", "snap_core")
+ c.Assert(err, IsNil)
+ c.Assert(m, DeepEquals, map[string]string{
+ "snap_mode": "",
+ "snap_core": "4",
+ })
+}
+
+func (s *PartitionTestSuite) TestGetBootloaderWithUboot(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ bootloader, err := FindBootloader()
+ c.Assert(err, IsNil)
+ c.Assert(bootloader, FitsTypeOf, &uboot{})
+}
+
+func (s *PartitionTestSuite) TestUbootSetEnvNoUselessWrites(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ envFile := (&uboot{}).envFile()
+ env, err := uenv.Create(envFile, 4096)
+ c.Assert(err, IsNil)
+ env.Set("snap_ab", "b")
+ env.Set("snap_mode", "")
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ st, err := os.Stat(envFile)
+ c.Assert(err, IsNil)
+ time.Sleep(100 * time.Millisecond)
+
+ u := newUboot()
+ c.Assert(u, NotNil)
+
+ // note that we set to the same var as above
+ err = u.SetBootVars(map[string]string{"snap_ab": "b"})
+ c.Assert(err, IsNil)
+
+ env, err = uenv.Open(envFile)
+ c.Assert(err, IsNil)
+ c.Assert(env.String(), Equals, "snap_ab=b\n")
+
+ st2, err := os.Stat(envFile)
+ c.Assert(err, IsNil)
+ c.Assert(st.ModTime(), Equals, st2.ModTime())
+}
+
+func (s *PartitionTestSuite) TestUbootSetBootVarFwEnv(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ err := u.SetBootVars(map[string]string{"key": "value"})
+ c.Assert(err, IsNil)
+
+ content, err := u.GetBootVars("key")
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, map[string]string{"key": "value"})
+}
+
+func (s *PartitionTestSuite) TestUbootGetBootVarFwEnv(c *C) {
+ s.makeFakeUbootEnv(c)
+
+ u := newUboot()
+ err := u.SetBootVars(map[string]string{"key2": "value2"})
+ c.Assert(err, IsNil)
+
+ content, err := u.GetBootVars("key2")
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, map[string]string{"key2": "value2"})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+// This is a var instead of a function to making mocking in the tests easier
+var runCommand = runCommandImpl
+
+// Run command specified by args and return the output
+func runCommandImpl(args ...string) (string, error) {
+ if len(args) == 0 {
+
+ return "", errors.New("no command specified")
+ }
+
+ cmd := exec.Command(args[0], args[1:]...)
+ stdout := bytes.NewBuffer(nil)
+ stderr := bytes.NewBuffer(nil)
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ err := cmd.Run()
+ if err != nil {
+ cmdline := strings.Join(args, " ")
+ return stdout.String(), fmt.Errorf("failed to run command %q: %q (%s)", cmdline, stderr, err)
+ }
+
+ return stdout.String(), err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package partition
+
+import (
+ . "gopkg.in/check.v1"
+)
+
+type UtilsTestSuite struct {
+}
+
+var _ = Suite(&UtilsTestSuite{})
+
+func (s *UtilsTestSuite) TestRunCommandSimple(c *C) {
+ output, err := runCommandImpl("sh", "-c", "printf 'foo\nbar'")
+ c.Assert(err, IsNil)
+ c.Assert(output, DeepEquals, "foo\nbar")
+}
+
+func (s *UtilsTestSuite) TestRunCommandWithStdoutReturnsFalse(c *C) {
+ _, err := runCommandImpl("false")
+ c.Assert(err, ErrorMatches, `failed to run command \"false\": \"\" \(exit status 1\)`)
+}
+
+func (s *UtilsTestSuite) TestRunCommandWithStdoutNoSuchCommand(c *C) {
+ _, err := runCommandImpl("no-such-command")
+ c.Assert(err, ErrorMatches, `failed to run command \"no-such-command\": \"\" \(exec: \"no-such-command\": executable file not found in \$PATH\)`)
+}
+
+func (s *UtilsTestSuite) TestRunCommandWithStdoutReturnsStdout(c *C) {
+ output, err := runCommandImpl("sh", "-c", "printf stdout ; printf 'stderr' >&2; false")
+ c.Assert(output, Matches, "stdout")
+ c.Assert(err, ErrorMatches, `failed to run command \".*\": \"stderr\" \(exit status 1\)`)
+}
--- /dev/null
+# German translation for snappy
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the snappy package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: snappy\n"
+"Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
+"POT-Creation-Date: 2016-12-14 12:38+0100\n"
+"PO-Revision-Date: 2015-09-02 22:57+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German <de@li.org>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2015-10-21 06:21+0000\n"
+"X-Generator: Launchpad (build 17812)\n"
+
+#. TRANSLATORS: 1. snap name, 2. snap version (keep those together please). the 3rd %s is a path (where it's mounted from).
+#, c-format
+msgid "%s %s mounted from %s\n"
+msgstr ""
+
+#, c-format
+msgid "%s (see \"snap login --help\")"
+msgstr ""
+
+#. TRANSLATORS: %s will be a message along the lines of "login required"
+#, c-format
+msgid "%s (try with sudo)"
+msgstr ""
+
+#, c-format
+msgid "%s already installed\n"
+msgstr ""
+
+#, c-format
+msgid "%s disabled\n"
+msgstr ""
+
+#, c-format
+msgid "%s enabled\n"
+msgstr ""
+
+#, c-format
+msgid "%s not installed\n"
+msgstr ""
+
+#, c-format
+msgid "%s removed\n"
+msgstr ""
+
+#, c-format
+msgid "%s reverted to %s\n"
+msgstr ""
+
+#, c-format
+msgid "%s%s %s from '%s' installed\n"
+msgstr ""
+
+#, c-format
+msgid "%s%s %s from '%s' upgraded\n"
+msgstr ""
+
+#, c-format
+msgid "%s%s %s installed\n"
+msgstr ""
+
+#, c-format
+msgid "%s%s %s upgraded\n"
+msgstr ""
+
+msgid "--list does not take mode nor channel flags"
+msgstr ""
+
+msgid "-r can only be used with --hook"
+msgstr ""
+
+msgid "<assertion file>"
+msgstr ""
+
+msgid "<assertion type>"
+msgstr ""
+
+msgid "<change-id>"
+msgstr ""
+
+msgid "<conf value>"
+msgstr ""
+
+#. TRANSLATORS: noun
+#. TRANSLATORS: noun
+msgid "<email>"
+msgstr ""
+
+msgid "<filename>"
+msgstr ""
+
+msgid "<header filter>"
+msgstr ""
+
+msgid "<key-name>"
+msgstr ""
+
+msgid "<key>"
+msgstr ""
+
+msgid "<model-assertion>"
+msgstr ""
+
+msgid "<query>"
+msgstr ""
+
+msgid "<root-dir>"
+msgstr ""
+
+msgid "<snap>:<plug>"
+msgstr ""
+
+msgid "<snap>:<slot or plug>"
+msgstr ""
+
+msgid "<snap>:<slot>"
+msgstr ""
+
+msgid "Abort a pending change"
+msgstr ""
+
+msgid "Adds an assertion to the system"
+msgstr ""
+
+msgid "Alias for --dangerous (DEPRECATED)"
+msgstr ""
+
+msgid "All snaps up to date."
+msgstr ""
+
+msgid "Alternative command to run"
+msgstr ""
+
+msgid "Always return document, even with single key"
+msgstr ""
+
+#. TRANSLATORS: note users on login.ubuntu.com can have multiple email addresses
+msgid "An email of a user on login.ubuntu.com"
+msgstr ""
+
+msgid "Assertion file"
+msgstr ""
+
+msgid "Assertion type name"
+msgstr ""
+
+msgid "Authenticates on snapd and the store"
+msgstr ""
+
+msgid "Bad code. Try again: "
+msgstr ""
+
+msgid "Buys a snap"
+msgstr ""
+
+msgid "Change ID"
+msgstr ""
+
+msgid "Changes configuration options"
+msgstr ""
+
+msgid ""
+"Classic dimension disabled on this system.\n"
+"Use \"sudo snap install --devmode classic && sudo classic.create\" to enable "
+"it."
+msgstr ""
+
+#, c-format
+msgid "Clear alias state for snap %q"
+msgstr ""
+
+msgid "Configuration value (key=value)"
+msgstr ""
+
+msgid "Confirm passphrase: "
+msgstr ""
+
+#, c-format
+msgid "Connect %s:%s to %s:%s"
+msgstr ""
+
+msgid "Connects a plug to a slot"
+msgstr ""
+
+msgid "Constrain listing to a specific snap or snap:name"
+msgstr ""
+
+msgid "Constrain listing to specific interfaces"
+msgstr ""
+
+msgid "Constrain listing to those matching header=value"
+msgstr ""
+
+#, c-format
+msgid "Copy snap %q data"
+msgstr ""
+
+msgid ""
+"Create a cryptographic key pair that can be used for signing assertions."
+msgstr ""
+
+msgid "Create cryptographic key pair"
+msgstr ""
+
+msgid "Create snap build assertion"
+msgstr ""
+
+msgid "Create snap-build assertion for the provided snap file."
+msgstr ""
+
+msgid "Creates a local system user"
+msgstr ""
+
+msgid "Delete cryptographic key pair"
+msgstr ""
+
+msgid "Delete the local cryptographic key pair with the given name."
+msgstr ""
+
+#, c-format
+msgid "Disable %q snap"
+msgstr ""
+
+msgid "Disables a snap in the system"
+msgstr ""
+
+#, c-format
+msgid "Discard interface connections for snap %q (%s)"
+msgstr ""
+
+#, c-format
+msgid "Disconnect %s:%s from %s:%s"
+msgstr ""
+
+msgid "Disconnects a plug from a slot"
+msgstr ""
+
+#, c-format
+msgid "Download snap %q%s from channel %q"
+msgstr ""
+
+msgid ""
+"Download the given revision of a snap, to which you must have developer "
+"access"
+msgstr ""
+
+msgid "Downloads the given snap"
+msgstr ""
+
+msgid "Email address: "
+msgstr ""
+
+#, c-format
+msgid "Enable %q snap"
+msgstr ""
+
+#, c-format
+msgid "Enable aliases for snap %q"
+msgstr ""
+
+msgid "Enables a snap in the system"
+msgstr ""
+
+msgid "Entering classic dimension"
+msgstr ""
+
+msgid ""
+"Export a public key assertion body that may be imported by other systems."
+msgstr ""
+
+msgid "Export cryptographic public key"
+msgstr ""
+
+#, c-format
+msgid "Fetch and check assertions for snap %q%s"
+msgstr ""
+
+#, c-format
+msgid "Fetching assertions for %q\n"
+msgstr ""
+
+#, c-format
+msgid "Fetching snap %q\n"
+msgstr ""
+
+msgid "Filename of the snap you want to assert a build for"
+msgstr ""
+
+msgid "Finds packages to install"
+msgstr ""
+
+msgid "Force adding the user, even if the device is already managed"
+msgstr ""
+
+msgid "Force import on classic systems"
+msgstr ""
+
+msgid ""
+"Format public key material as a request for an account-key for this account-"
+"id"
+msgstr ""
+
+msgid "Generate device key"
+msgstr ""
+
+msgid "Generate the manpage"
+msgstr ""
+
+msgid "Grade states the build quality of the snap (defaults to 'stable')"
+msgstr ""
+
+msgid "Grant sudo access to the created user"
+msgstr ""
+
+msgid "Help"
+msgstr ""
+
+msgid "Hook to run"
+msgstr ""
+
+msgid "ID\tStatus\tSpawn\tReady\tSummary\n"
+msgstr ""
+
+msgid "ISO 4217 code for currency (https://en.wikipedia.org/wiki/ISO_4217)"
+msgstr ""
+
+msgid "Identifier of the signer"
+msgstr ""
+
+msgid "Identifier of the snap package associated with the build"
+msgstr ""
+
+msgid "Ignore validation by other snaps blocking the refresh"
+msgstr ""
+
+msgid "Include a verbose list of a snap's notes (otherwise, summarise notes)"
+msgstr ""
+
+msgid "Initialize device"
+msgstr ""
+
+msgid "Inspects devices for actionable information"
+msgstr ""
+
+#, c-format
+msgid "Install %q snap"
+msgstr ""
+
+#, c-format
+msgid "Install %q snap from %q channel"
+msgstr ""
+
+#, c-format
+msgid "Install %q snap from file"
+msgstr ""
+
+#, c-format
+msgid "Install %q snap from file %q"
+msgstr ""
+
+msgid "Install from the beta channel"
+msgstr ""
+
+msgid "Install from the candidate channel"
+msgstr ""
+
+msgid "Install from the edge channel"
+msgstr ""
+
+msgid "Install from the stable channel"
+msgstr ""
+
+#, c-format
+msgid "Install snap %q"
+msgstr ""
+
+#. TRANSLATORS: the %s is a comma-separated list of quoted snap names
+#, c-format
+msgid "Install snaps %s"
+msgstr ""
+
+msgid ""
+"Install the given revision of a snap, to which you must have developer access"
+msgstr ""
+
+msgid ""
+"Install the given snap file even if there are no pre-acknowledged signatures "
+"for it, meaning it was not verified and could be dangerous (--devmode "
+"implies this)"
+msgstr ""
+
+msgid "Installs a snap to the system"
+msgstr ""
+
+msgid "Key of interest within the configuration"
+msgstr ""
+
+msgid "List a change's tasks"
+msgstr ""
+
+msgid "List cryptographic keys"
+msgstr ""
+
+msgid "List cryptographic keys that can be used for signing assertions."
+msgstr ""
+
+msgid "List installed snaps"
+msgstr ""
+
+msgid "List system changes"
+msgstr ""
+
+msgid "Lists interfaces in the system"
+msgstr ""
+
+msgid "Log out of the store"
+msgstr ""
+
+msgid "Login successful"
+msgstr ""
+
+#, c-format
+msgid "Make current revision for snap %q unavailable"
+msgstr ""
+
+#, c-format
+msgid "Make snap %q (%s) available to the system"
+msgstr ""
+
+#, c-format
+msgid "Make snap %q (%s) unavailable to the system"
+msgstr ""
+
+#, c-format
+msgid "Make snap %q unavailable to the system"
+msgstr ""
+
+#, c-format
+msgid "Make snap %q%s available to the system"
+msgstr ""
+
+msgid "Mark system seeded"
+msgstr ""
+
+#, c-format
+msgid "Mount snap %q%s"
+msgstr ""
+
+msgid "Name of key to create; defaults to 'default'"
+msgstr ""
+
+msgid "Name of key to delete"
+msgstr ""
+
+msgid "Name of key to export"
+msgstr ""
+
+msgid "Name of the GnuPG key to use (defaults to 'default' as key name)"
+msgstr ""
+
+msgid "Name of the key to use, otherwise use the default key"
+msgstr ""
+
+msgid "Name\tSHA3-384"
+msgstr ""
+
+msgid "Name\tVersion\tDeveloper\tNotes\tSummary"
+msgstr ""
+
+msgid "Name\tVersion\tRev\tDeveloper\tNotes"
+msgstr ""
+
+msgid "No snaps are installed yet. Try \"snap install hello-world\"."
+msgstr ""
+
+msgid "Output results in JSON format"
+msgstr ""
+
+msgid "Passphrase: "
+msgstr ""
+
+#, c-format
+msgid "Password of %q: "
+msgstr ""
+
+#. TRANSLATORS: %q, %q and %s are the snap name, developer, and price. Please wrap the translation at 80 characters.
+#, c-format
+msgid ""
+"Please re-enter your Ubuntu One password to purchase %q from %q\n"
+"for %s. Press ctrl-c to cancel."
+msgstr ""
+
+#, c-format
+msgid ""
+"Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms "
+"and conditions.\n"
+"Once completed, return here and run 'snap buy %s' again."
+msgstr ""
+
+msgid "Prepare a snappy image"
+msgstr ""
+
+#, c-format
+msgid "Prepare snap %q (%s)"
+msgstr ""
+
+#, c-format
+msgid "Prepare snap %q%s"
+msgstr ""
+
+msgid "Print the version and exit"
+msgstr ""
+
+msgid "Prints configuration options"
+msgstr ""
+
+msgid "Prints whether system is managed"
+msgstr ""
+
+msgid "Put snap in classic mode and disable security confinement"
+msgstr ""
+
+msgid "Put snap in development mode and disable security confinement"
+msgstr ""
+
+msgid "Put snap in enforced confinement mode"
+msgstr ""
+
+#, c-format
+msgid "Refresh %q snap"
+msgstr ""
+
+#, c-format
+msgid "Refresh %q snap from %q channel"
+msgstr ""
+
+msgid "Refresh all snaps: no updates"
+msgstr ""
+
+#, c-format
+msgid "Refresh snap %q"
+msgstr ""
+
+#. TRANSLATORS: the %s is a comma-separated list of quoted snap names
+#, c-format
+msgid "Refresh snaps %s"
+msgstr ""
+
+msgid "Refresh to the given revision"
+msgstr ""
+
+msgid "Refreshes a snap in the system"
+msgstr ""
+
+#, c-format
+msgid "Remove %q snap"
+msgstr ""
+
+#, c-format
+msgid "Remove aliases for snap %q"
+msgstr ""
+
+#, c-format
+msgid "Remove data for snap %q (%s)"
+msgstr ""
+
+msgid "Remove only the given revision"
+msgstr ""
+
+#, c-format
+msgid "Remove security profile for snap %q (%s)"
+msgstr ""
+
+#, c-format
+msgid "Remove snap %q"
+msgstr ""
+
+#, c-format
+msgid "Remove snap %q (%s) from the system"
+msgstr ""
+
+#. TRANSLATORS: the %s is a comma-separated list of quoted snap names
+#, c-format
+msgid "Remove snaps %s"
+msgstr ""
+
+msgid "Removes a snap from the system"
+msgstr ""
+
+msgid "Request device serial"
+msgstr ""
+
+msgid "Restrict the search to a given section"
+msgstr ""
+
+#, c-format
+msgid "Revert %q snap"
+msgstr ""
+
+msgid "Reverts the given snap to the previous state"
+msgstr ""
+
+msgid "Run a shell instead of the command (useful for debugging)"
+msgstr ""
+
+#, c-format
+msgid "Run configure hook of %q snap"
+msgstr ""
+
+#, c-format
+msgid "Run configure hook of %q snap if present"
+msgstr ""
+
+msgid "Run prepare-device hook"
+msgstr ""
+
+msgid "Run the given snap command"
+msgstr ""
+
+msgid "Run the given snap command with the right confinement and environment"
+msgstr ""
+
+msgid "Runs unsupported experimental commands"
+msgstr ""
+
+msgid "Search private snaps"
+msgstr ""
+
+#, c-format
+msgid "Setup snap %q aliases"
+msgstr ""
+
+#, c-format
+msgid "Setup snap %q%s security profiles"
+msgstr ""
+
+msgid "Show all revisions"
+msgstr ""
+
+msgid "Show available snaps for refresh"
+msgstr ""
+
+msgid "Shows known assertions of the provided type"
+msgstr ""
+
+msgid "Shows version details"
+msgstr ""
+
+msgid "Sign an assertion"
+msgstr ""
+
+msgid ""
+"Sign an assertion using the specified key, using the input for headers from "
+"a JSON mapping provided through stdin, the body of the assertion can be "
+"specified through a \"body\" pseudo-header.\n"
+msgstr ""
+
+msgid "Slot\tPlug"
+msgstr ""
+
+msgid "Snap name"
+msgstr ""
+
+msgid ""
+"Sorry, your payment method has been declined by the issuer. Please review "
+"your\n"
+"payment details at https://my.ubuntu.com/payment/edit and try again."
+msgstr ""
+
+#, c-format
+msgid "Start snap %q (%s) services"
+msgstr ""
+
+#, c-format
+msgid "Start snap %q%s services"
+msgstr ""
+
+msgid "Start snap services"
+msgstr ""
+
+msgid "Status\tSpawn\tReady\tSummary\n"
+msgstr ""
+
+#, c-format
+msgid "Stop snap %q (%s) services"
+msgstr ""
+
+#, c-format
+msgid "Stop snap %q services"
+msgstr ""
+
+msgid "Stop snap services"
+msgstr ""
+
+msgid "Strict typing with nulls and quoted strings"
+msgstr ""
+
+msgid "Temporarily mount device before inspecting"
+msgstr ""
+
+msgid "Tests a snap in the system"
+msgstr ""
+
+#. TRANSLATORS: %q and %s are the same snap name. Please wrap the translation at 80 characters.
+#, c-format
+msgid ""
+"Thanks for purchasing %q. You may now install it on any of your devices\n"
+"with 'snap install %s'."
+msgstr ""
+
+msgid "The login.ubuntu.com email to login as"
+msgstr ""
+
+msgid "The model assertion name"
+msgstr ""
+
+msgid "The output directory"
+msgstr ""
+
+msgid "The snap to configure (e.g. hello-world)"
+msgstr ""
+
+msgid "The snap whose conf is being requested"
+msgstr ""
+
+msgid "This command logs the current user out of the store"
+msgstr ""
+
+msgid "Tool to interact with snaps"
+msgstr ""
+
+#, c-format
+msgid "Try %q snap from %s"
+msgstr ""
+
+msgid "Two-factor code: "
+msgstr ""
+
+msgid "Use a specific snap revision when running hook"
+msgstr ""
+
+msgid "Use known assertions for user creation"
+msgstr ""
+
+msgid "Use this channel instead of stable"
+msgstr ""
+
+#, c-format
+msgid "WARNING: failed to activate logging: %v\n"
+msgstr ""
+
+msgid "Waiting for server to restart"
+msgstr ""
+
+msgid "Watch a change in progress"
+msgstr ""
+
+msgid "Wrong again. Once more: "
+msgstr ""
+
+msgid "Yes, yes it does."
+msgstr "Ja, ja, allerdings."
+
+#, c-format
+msgid ""
+"You do not have a payment method associated with your account, visit https://"
+"my.ubuntu.com/payment/edit to add one.\n"
+"Once completed, return here and run 'snap buy %s' again."
+msgstr ""
+
+msgid ""
+"You need to be logged in to purchase software. Please run 'snap login' and "
+"try again."
+msgstr ""
+
+#. TRANSLATORS: the %s is the argument given by the user to "snap changes"
+#, c-format
+msgid "\"snap changes\" command expects a snap name, try: \"snap change %s\""
+msgstr ""
+
+msgid ""
+"\n"
+"Install, configure, refresh and remove snap packages. Snaps are\n"
+"'universal' packages that work across many different Linux systems,\n"
+"enabling secure distribution of the latest apps and utilities for\n"
+"cloud, servers, desktops and the internet of things.\n"
+"\n"
+"This is the CLI for snapd, a background service that takes care of\n"
+"snaps on the system. Start with 'snap list' to see installed snaps.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The abort command attempts to abort a change that still has pending tasks.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The ack command tries to add an assertion to the system assertion database.\n"
+"\n"
+"The assertion may also be a newer revision of a preexisting assertion that "
+"it\n"
+"will replace.\n"
+"\n"
+"To succeed the assertion must be valid, its signature verified with a known\n"
+"public key and the assertion consistent with and its prerequisite in the\n"
+"database.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The auto-import command searches available mounted devices looking for\n"
+"assertions that are signed by trusted authorities, and potentially\n"
+"performs system changes based on them.\n"
+"\n"
+"If one or more device paths are provided via --mount, these are temporariy\n"
+"mounted to be inspected as well. Even in that case the command will still\n"
+"consider all available mounted devices for inspection.\n"
+"\n"
+"Imported assertions must be made available in the auto-import.assert file\n"
+"in the root of the filesystem.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The buy command buys a snap from the store.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The change command displays a summary of tasks associated to an individual "
+"change."
+msgstr ""
+
+msgid ""
+"\n"
+"The changes command displays a summary of the recent system changes "
+"performed."
+msgstr ""
+
+msgid ""
+"\n"
+"The connect command connects a plug to a slot.\n"
+"It may be called in the following ways:\n"
+"\n"
+"$ snap connect <snap>:<plug> <snap>:<slot>\n"
+"\n"
+"Connects the provided plug to the given slot.\n"
+"\n"
+"$ snap connect <snap>:<plug> <snap>\n"
+"\n"
+"Connects the specific plug to the only slot in the provided snap that "
+"matches\n"
+"the connected interface. If more than one potential slot exists, the "
+"command\n"
+"fails.\n"
+"\n"
+"$ snap connect <snap>:<plug>\n"
+"\n"
+"Connects the provided plug to the slot in the core snap with a name "
+"matching\n"
+"the plug name.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The create-user command creates a local system user with the username and "
+"SSH\n"
+"keys registered on the store account identified by the provided email "
+"address.\n"
+"\n"
+"An account can be setup at https://login.ubuntu.com.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The disable command disables a snap. The binaries and services of the\n"
+"snap will no longer be available. But all the data is still available\n"
+"and the snap can easily be enabled again.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The disconnect command disconnects a plug from a slot.\n"
+"It may be called in the following ways:\n"
+"\n"
+"$ snap disconnect <snap>:<plug> <snap>:<slot>\n"
+"\n"
+"Disconnects the specific plug from the specific slot.\n"
+"\n"
+"$ snap disconnect <snap>:<slot or plug>\n"
+"\n"
+"Disconnects everything from the provided plug or slot.\n"
+"The snap name may be omitted for the core snap.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The download command downloads the given snap and its supporting assertions\n"
+"to the current directory under .snap and .assert file extensions, "
+"respectively.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The enable command enables a snap that was previously disabled.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The experimental command contains a selection of additional sub-commands.\n"
+"\n"
+"Experimental commands can be removed without notice and may not work on\n"
+"non-development systems.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The find command queries the store for available packages.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The get command prints configuration options for the current snap.\n"
+"\n"
+" $ snapctl get username\n"
+" frank\n"
+"\n"
+"If multiple option names are provided, a document is returned:\n"
+"\n"
+" $ snapctl get username password\n"
+" {\n"
+" \"username\": \"frank\",\n"
+" \"password\": \"...\"\n"
+" }\n"
+"\n"
+"Nested values may be retrieved via a dotted path:\n"
+"\n"
+" $ snapctl get author.name\n"
+" frank\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The get command prints configuration options for the provided snap.\n"
+"\n"
+" $ snap get snap-name username\n"
+" frank\n"
+"\n"
+"If multiple option names are provided, a document is returned:\n"
+"\n"
+" $ snap get snap-name username password\n"
+" {\n"
+" \"username\": \"frank\",\n"
+" \"password\": \"...\"\n"
+" }\n"
+"\n"
+"Nested values may be retrieved via a dotted path:\n"
+"\n"
+" $ snap get snap-name author.name\n"
+" frank\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The help command shows helpful information. Unlike this. ;-)\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The info command shows detailed information about a snap, be it by name or "
+"by path."
+msgstr ""
+
+msgid ""
+"\n"
+"The install command installs the named snap in the system.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The interfaces command lists interfaces available in the system.\n"
+"\n"
+"By default all slots and plugs, used and offered by all snaps, are "
+"displayed.\n"
+" \n"
+"$ snap interfaces <snap>:<slot or plug>\n"
+"\n"
+"Lists only the specified slot or plug.\n"
+"\n"
+"$ snap interfaces <snap>\n"
+"\n"
+"Lists the slots offered and plugs used by the specified snap.\n"
+"\n"
+"$ snap interfaces -i=<interface> [<snap>]\n"
+"\n"
+"Filters the complete output so only plugs and/or slots matching the provided "
+"details are listed.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The known command shows known assertions of the provided type.\n"
+"If header=value pairs are provided after the assertion type, the assertions\n"
+"shown must also have the specified headers matching the provided values.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The list command displays a summary of snaps installed in the current system."
+msgstr ""
+
+msgid ""
+"\n"
+"The login command authenticates on snapd and the snap store and saves "
+"credentials\n"
+"into the ~/.snap/auth.json file. Further communication with snapd will then "
+"be made\n"
+"using those credentials.\n"
+"\n"
+"Login only works for local users in the sudo, admin or wheel groups.\n"
+"\n"
+"An account can be setup at https://login.ubuntu.com\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The managed command will print true or false informing whether\n"
+"snapd has registered users.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The refresh command refreshes (updates) the named snap.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The remove command removes the named snap from the system.\n"
+"\n"
+"By default all the snap revisions are removed, including their data and the "
+"common\n"
+"data directory. When a --revision option is passed only the specified "
+"revision is\n"
+"removed.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The revert command reverts the given snap to its state before\n"
+"the latest refresh. This will reactivate the previous snap revision,\n"
+"and will use the original data that was associated with that revision,\n"
+"discarding any data changes that were done by the latest revision. As\n"
+"an exception, data which the snap explicitly chooses to share across\n"
+"revisions is not touched by the revert process.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The set command changes the provided configuration options as requested.\n"
+"\n"
+" $ snap set snap-name username=frank password=$PASSWORD\n"
+"\n"
+"All configuration changes are persisted at once, and only after the\n"
+"snap's configuration hook returns successfully.\n"
+"\n"
+"Nested values may be modified via a dotted path:\n"
+"\n"
+" $ snap set author.name=frank\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The set command changes the provided configuration options as requested.\n"
+"\n"
+" $ snapctl set username=frank password=$PASSWORD\n"
+"\n"
+"All configuration changes are persisted at once, and only after the hook\n"
+"returns successfully.\n"
+"\n"
+"Nested values may be modified via a dotted path:\n"
+"\n"
+" $ snapctl set author.name=frank\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The try command installs an unpacked snap into the system for testing "
+"purposes.\n"
+"The unpacked snap content continues to be used even after installation, so\n"
+"non-metadata changes there go live instantly. Metadata changes such as "
+"those\n"
+"performed in snap.yaml will require reinstallation to go live.\n"
+"\n"
+"If snap-dir argument is omitted, the try command will attempt to infer it "
+"if\n"
+"either snapcraft.yaml file and prime directory or meta/snap.yaml file can "
+"be\n"
+"found relative to current working directory.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The version command displays the versions of the running client, server,\n"
+"and operating system.\n"
+msgstr ""
+
+msgid ""
+"\n"
+"The watch command waits for the given change-id to finish and shows "
+"progress\n"
+"(if available).\n"
+msgstr ""
+
+msgid ""
+"\n"
+"\n"
+"The home directory is shared between snappy and the classic dimension.\n"
+"Run \"exit\" to leave the classic shell.\n"
+msgstr ""
+
+msgid "a single snap name is needed to specify mode or channel flags"
+msgstr ""
+
+msgid "a single snap name is needed to specify the revision"
+msgstr ""
+
+msgid "a single snap name must be specified when ignoring validation"
+msgstr ""
+
+msgid "bought"
+msgstr ""
+
+#. TRANSLATORS: if possible, a single short word
+msgid "broken"
+msgstr ""
+
+#, c-format
+msgid "cannot buy snap: %v"
+msgstr ""
+
+msgid "cannot buy snap: invalid characters in name"
+msgstr ""
+
+msgid "cannot buy snap: it has already been bought"
+msgstr ""
+
+#. TRANSLATORS: %q is the directory whose creation failed, %v the error message
+#, c-format
+msgid "cannot create %q: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot create assertions file: %v"
+msgstr ""
+
+#. TRANSLATORS: %q gets the snap name, %v gets the resulting error message
+#, c-format
+msgid "cannot extract the snap-name from local file %q: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot find app %q in %q"
+msgstr ""
+
+#, c-format
+msgid "cannot find hook %q in %q"
+msgstr ""
+
+#. TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it
+#. TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it
+#, c-format
+msgid "cannot get data for %q: %v"
+msgstr ""
+
+#. TRANSLATORS: %q gets what the user entered, %v gets the resulting error message
+#, c-format
+msgid "cannot get full path for %q: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot get the current user: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot mark boot successful: %s"
+msgstr ""
+
+#, c-format
+msgid "cannot open the assertions database: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot read assertion input: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot resolve snap app %q: %v"
+msgstr ""
+
+#, c-format
+msgid "cannot sign assertion: %v"
+msgstr ""
+
+#. TRANSLATORS: %q is the key name, %v the error message
+#, c-format
+msgid "cannot use %q key: %v"
+msgstr ""
+
+msgid "cannot use --hook and --command together"
+msgstr ""
+
+msgid "cannot use devmode and jailmode flags together"
+msgstr ""
+
+#, c-format
+msgid "change finished in status %q with no error message"
+msgstr ""
+
+#, c-format
+msgid "created user %q\n"
+msgstr ""
+
+#. TRANSLATORS: if possible, a single short word
+msgid "disabled"
+msgstr ""
+
+#, c-format
+msgid "error: %v\n"
+msgstr ""
+
+msgid ""
+"error: the `<snap-dir>` argument was not provided and couldn't be inferred"
+msgstr ""
+
+#, c-format
+msgid "internal error, please report: running %q failed: %v\n"
+msgstr ""
+
+#, c-format
+msgid "invalid attribute: %q (want key=value)"
+msgstr ""
+
+#, c-format
+msgid "invalid configuration: %q (want key=value)"
+msgstr ""
+
+#, c-format
+msgid "invalid header filter: %q (want key=value)"
+msgstr ""
+
+#, c-format
+msgid "invalid parameter: %q (want key=value)"
+msgstr ""
+
+#, c-format
+msgid "invalid value: %q (want snap:name or snap)"
+msgstr ""
+
+#, c-format
+msgid ""
+"key name %q is not valid; only ASCII letters, digits, and hyphens are allowed"
+msgstr ""
+
+msgid "need the application to run as argument"
+msgstr ""
+
+msgid "no changes found"
+msgstr ""
+
+msgid "no interfaces found"
+msgstr ""
+
+msgid "no matching snaps installed"
+msgstr ""
+
+#. TRANSLATORS: the %q is the (quoted) query the user entered
+#, c-format
+msgid "no snaps found for %q"
+msgstr ""
+
+msgid "no valid snaps given"
+msgstr ""
+
+msgid "not a valid snap"
+msgstr ""
+
+#. TRANSLATORS: if possible, a single short word
+msgid "private"
+msgstr ""
+
+msgid ""
+"reboot scheduled to update the system - temporarily cancel with 'sudo "
+"shutdown -c'"
+msgstr ""
+
+msgid "show detailed information about a snap"
+msgstr ""
+
+#. TRANSLATORS: free as in gratis
+msgid "snap is free"
+msgstr ""
+
+msgid "too many arguments for command"
+msgstr ""
+
+#. TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments
+#, c-format
+msgid "too many arguments for hook %q: %s"
+msgstr ""
+
+#. TRANSLATORS: the %s is the list of extra arguments
+#, c-format
+msgid "too many arguments: %s"
+msgstr ""
+
+msgid "unavailable"
+msgstr ""
+
+#, c-format
+msgid "unknown command %q, see \"snap --help\""
+msgstr ""
+
+#, c-format
+msgid "unsupported shell %v"
+msgstr ""
--- /dev/null
+# Spanish translation for snappy
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the snappy package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: snappy\n"
+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
+"POT-Creation-Date: 2015-10-15 15:53+0200\n"
+"PO-Revision-Date: 2015-09-13 12:42+0000\n"
+"Last-Translator: Adolfo Jayme <fitoschido@gmail.com>\n"
+"Language-Team: Spanish <es@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2015-10-21 06:21+0000\n"
+"X-Generator: Launchpad (build 17812)\n"
+
+#. TRANSLATORS: the %s is a pkgname, the second a comma separated list of paths
+#, c-format
+msgid "%s: %s\n"
+msgstr "%s: %s\n"
+
+#. TRANSLATORS: the %s stand for "name", "version", "description"
+#, c-format
+msgid "%s\t%s\t%s (forks not shown: %d)\t"
+msgstr "%s\t%s\t%s (bifurcaciones no mostradas: %d)\t"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' is no longer allowed to access '%s'\n"
+msgstr "«%s» ya no está autorizado para acceder a «%s»\n"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' is now allowed to access '%s'\n"
+msgstr "«%s» ahora tiene autorización para acceder a «%s»\n"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' previously allowed access to '%s'. Skipping\n"
+msgstr "«%s» ya estaba autorizado para acceder a «%s». Omitido\n"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "'%s:' is not allowed to access additional hardware\n"
+msgstr "«%s»: no está autorizado para acceder a hardware adicional\n"
+
+msgid "(deprecated) please use \"list\""
+msgstr "(obsoleto) use «list»"
+
+msgid "2fa code: "
+msgstr "Código 2fa: "
+
+msgid ""
+"A concise summary of key attributes of the snappy system, such as the "
+"release and channel.\n"
+"\n"
+"The verbose output includes the specific version information for the factory "
+"image, the running image and the image that will be run on reboot, together "
+"with a list of the available channels for this image.\n"
+"\n"
+"Providing a package name will display information about a specific installed "
+"package.\n"
+"\n"
+"The verbose version of the info command for a package will also tell you the "
+"available channels for that package, when it was installed for the first "
+"time, disk space utilization, and in the case of frameworks, which apps are "
+"able to use the framework."
+msgstr ""
+
+msgid "Activate a package"
+msgstr ""
+
+msgid ""
+"Activate a package that has previously been deactivated. If the package is "
+"already activated, do nothing."
+msgstr ""
+
+msgid ""
+"Allows rollback of a snap to a previous installed version. Without any "
+"arguments, the previous installed version is selected. It is also possible "
+"to specify the version to rollback to as a additional argument.\n"
+msgstr ""
+"Permite volver a una versión de snap instalada anteriormente. Sin "
+"argumentos, se selecciona la versión anterior. Es posible especificar la "
+"versión a la que volver como un argumento adicional.\n"
+
+msgid "Assign a hardware device to a package"
+msgstr "Asignar un dispositivo de hardware a un paquete"
+
+msgid "Assign hardware to a specific installed package"
+msgstr ""
+
+msgid "Builds a snap package"
+msgstr "Construye un paquete snap"
+
+#. TRANSLATORS: the first %q is the file that can not be read and %v is the error message
+#, c-format
+msgid "Can't read hook file %q: %v"
+msgstr "No se puede leer el archivo de «hook» %q: %v"
+
+msgid ""
+"Configures a package. The configuration is a YAML file, provided in the "
+"specified file which can be \"-\" for stdin. Output of the command is the "
+"current configuration, so running this command with no input file provides a "
+"snapshot of the app's current config."
+msgstr ""
+"Configura un paquete. La configuración es un archivo YAML, suministrado en "
+"el archivo especificado que puede ser «-» para stdin. La salida de la orden "
+"es la configuración actual, de forma que ejecutar esta orden sin el archivo "
+"brinda una copia de la configuración actual de la aplicación."
+
+msgid "Creates a snap package and if available, runs the review scripts."
+msgstr ""
+"Crea un paquete snap y, si están disponibles, ejecuta los «scripts» de "
+"revisión."
+
+msgid "Deactivate a package"
+msgstr ""
+
+msgid ""
+"Deactivate a package. If the package is already deactivated, do nothing."
+msgstr ""
+
+msgid "Display a summary of key attributes of the snappy system."
+msgstr "Muestra un resumen de los atributos clave de el sistema snappy."
+
+msgid "Do not clean up old versions of the package."
+msgstr ""
+
+msgid "Ensures system is running with latest parts"
+msgstr ""
+"Se asegura que el sistema está ejecutando con las partes más recientes"
+
+msgid "First boot has already run"
+msgstr "El primer arranque ya se ejecutó"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Generated '%s' snap\n"
+msgstr "Snap «%s» generado\n"
+
+msgid "Include information about packages from the snappy store"
+msgstr ""
+
+msgid "Install a snap package"
+msgstr "Instalar un paquete snap"
+
+msgid "Install snaps even if the signature can not be verified."
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Installing %s\n"
+msgstr "Instalando %s\n"
+
+msgid "List active components installed on a snappy system"
+msgstr "Listar los componentes activos instalados en un sistema snappy"
+
+msgid "List assigned hardware device for a package"
+msgstr "Listar dispositivos de hardware asignados para un paquete"
+
+msgid "List assigned hardware for a specific installed package"
+msgstr ""
+
+msgid "Log into the store"
+msgstr "Iniciar sesión en la tienda"
+
+msgid "Login successful"
+msgstr "Inicio de sesión correcto"
+
+msgid "Name\tDate\tVersion\t"
+msgstr "Nombre\tFecha\tVersión\t"
+
+msgid "Name\tDate\tVersion\tDeveloper\t"
+msgstr "Nombre\tFecha\tVersión\tDesarrollador\t"
+
+msgid "Name\tVersion\tSummary\t"
+msgstr "Nombre\tVersión\tResumen\t"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "No snap: '%s' found"
+msgstr "Nigún snap: «%s» encontrado"
+
+msgid "Password: "
+msgstr "Contraseña: "
+
+msgid "Provide information about a specific installed package"
+msgstr ""
+
+msgid ""
+"Provides a list of all active components installed on a snappy system.\n"
+"\n"
+"If requested, the command will find out if there are updates for any of the "
+"components and indicate that by appending a * to the date. This will be "
+"slower as it requires a round trip to the app store on the network.\n"
+"\n"
+"The developer information refers to non-mainline versions of a package (much "
+"like PPAs in deb-based Ubuntu). If the package is the primary version of "
+"that package in Ubuntu then the developer info is not shown. This allows one "
+"to identify packages which have custom, non-standard versions installed. As "
+"a special case, the \"sideload\" developer refers to packages installed "
+"manually on the system.\n"
+"\n"
+"When a verbose listing is requested, information about the channel used is "
+"displayed; which is one of alpha, beta, rc or stable, and all fields are "
+"fully expanded too. In some cases, older (inactive) versions of snappy "
+"packages will be installed, these will be shown in the verbose output and "
+"the active version indicated with a * appended to the name of the component."
+msgstr ""
+
+msgid "Provides more detailed information"
+msgstr ""
+
+msgid "Purge an installed package."
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Purging %s\n"
+msgstr "Purgando %s\n"
+
+msgid "Query and modify snappy services"
+msgstr ""
+
+msgid "Query and modify snappy services of locally-installed packages"
+msgstr ""
+
+msgid "Query the store for available packages"
+msgstr "Consultar la tienda por paquetes disponibles"
+
+msgid "Reboot if necessary to be on the latest running system."
+msgstr ""
+
+#. TRANSLATORS: the first %s is a pkgname the second a version
+#, c-format
+msgid "Reboot to use %s version %s."
+msgstr "Reinicie para usar %s versión %s."
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Reboot to use the new %s."
+msgstr "Reinicie para usar el nuevo %s."
+
+#. TRANSLATORS: the %s shows a comma separated list
+#. of package names
+#, c-format
+msgid "Rebooting to satisfy updates for %s\n"
+msgstr "Reiniciando para satisfacer las actualizaciones para %s\n"
+
+msgid "Remove a snapp part"
+msgstr "Eliminar una parte de snap"
+
+msgid "Remove all the data from the listed packages"
+msgstr "Eliminar todos los datos de los paquetes listados"
+
+msgid ""
+"Remove all the data from the listed packages. Normally this is used for "
+"packages that have been removed and attempting to purge data for an "
+"installed package will result in an error. The --installed option overrides "
+"that and enables the administrator to purge all data for an installed "
+"package (effectively resetting the package completely)."
+msgstr ""
+
+msgid "Remove hardware from a specific installed package"
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Removing %s\n"
+msgstr "Eliminando %s\n"
+
+msgid "Rollback to a previous version of a package"
+msgstr "Volver a una versión anterior de un paquete"
+
+msgid "Search for packages to install"
+msgstr "Buscar paquetes para instalar"
+
+msgid "Set configuration for a specific installed package"
+msgstr ""
+
+msgid "Set configuration for an installed package."
+msgstr "Establecer la configuración de un paquete instalado"
+
+msgid "Set properties of system or package"
+msgstr "Establecer las propiedades de un sistema o paquete"
+
+msgid ""
+"Set properties of system or package\n"
+"\n"
+"Supported properties are:\n"
+" active=VERSION\n"
+"\n"
+"Example:\n"
+" set hello-world active=1.0\n"
+msgstr ""
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is the new version
+#, c-format
+msgid "Setting %s to version %s\n"
+msgstr "Estableciendo %s a la versión %s\n"
+
+msgid "Show all available forks of a package"
+msgstr ""
+
+msgid "Show available updates (requires network)"
+msgstr ""
+
+msgid "Show channel information and expand all fields"
+msgstr ""
+
+msgid "Snap\tService\tState"
+msgstr ""
+
+msgid "Specify an alternate output directory for the resulting package"
+msgstr ""
+
+msgid "The Package to install (name or path)"
+msgstr ""
+
+msgid ""
+"The \"versions\" command is no longer available.\n"
+"\n"
+"Please use the \"list\" command instead to see what is installed.\n"
+"The \"list -u\" (or \"list --updates\") will show you the available updates\n"
+"and \"list -v\" (or \"list --verbose\") will show all installed versions.\n"
+msgstr ""
+
+msgid "The configuration for the given file"
+msgstr ""
+
+msgid "The configuration for the given install"
+msgstr ""
+
+msgid "The hardware device path (e.g. /dev/ttyUSB0)"
+msgstr ""
+
+msgid "The package to rollback "
+msgstr ""
+
+msgid "The version to rollback to"
+msgstr ""
+
+msgid ""
+"This command adds access to a specific hardware device (e.g. /dev/ttyUSB0) "
+"for an installed package."
+msgstr ""
+"Esta orden agrega el acceso a un dispositivo hardware específico (p.ej. "
+"/dev/ttyUSB0) para un paquete instalado."
+
+msgid "This command is no longer available, please use the \"list\" command"
+msgstr "Esta orden ya no está disponible. Utilice «list» en su lugar"
+
+msgid "This command list what hardware an installed package can access"
+msgstr ""
+"Esta orden lista el hardware al que tiene acceso un paquete instalado"
+
+msgid "This command logs the given username into the store"
+msgstr "Esta orden inicia una sesión del usuario especificado en la tienda"
+
+msgid ""
+"This command removes access of a specific hardware device (e.g. "
+"/dev/ttyUSB0) for an installed package."
+msgstr ""
+"Esta orden elimina el acceso a un dispositivo de hardware específico (p.ej. "
+"/dev/ttyUSB0) para un paquete instalado."
+
+msgid "Unassign a hardware device to a package"
+msgstr "Desasignar un dispositivo de hardware a un paquete"
+
+msgid "Update all installed parts"
+msgstr "Actualizar todas las partes intaladas"
+
+msgid "Use --show-all to see all available forks."
+msgstr "Use --show-all para ver todas las bifurcaciones disponibles."
+
+msgid "Username for the login"
+msgstr ""
+
+#. TRANSLATORS: the %s represents a list of installed appnames
+#. (e.g. "apps: foo, bar, baz")
+#, c-format
+msgid "apps: %s\n"
+msgstr "aplicaciones: %s\n"
+
+#. TRANSLATORS: the %s an architecture string
+#, c-format
+msgid "architecture: %s\n"
+msgstr "arquitectura: %s\n"
+
+#. TRANSLATORS: the %s is a size
+#, c-format
+msgid "binary-size: %v\n"
+msgstr "tamaño del binario: %v\n"
+
+#. TRANSLATORS: the %s is a channel name
+#, c-format
+msgid "channel: %s\n"
+msgstr "canal: %s\n"
+
+#. TRANSLATORS: the %s is a size
+#, c-format
+msgid "data-size: %s\n"
+msgstr "tamaño de los datos: %s\n"
+
+#. TRANSLATORS: the %s is a comma separated list of framework names
+#, c-format
+msgid "frameworks: %s\n"
+msgstr "marcos: %s\n"
+
+#. TRANSLATORS: the %s is a date
+#, c-format
+msgid "installed: %s\n"
+msgstr "instalado: %s\n"
+
+msgid "package name is required"
+msgstr "se requiere el nombre del paquete"
+
+msgid "produces manpage"
+msgstr ""
+
+#. TRANSLATORS: the %s release string
+#, c-format
+msgid "release: %s\n"
+msgstr "publicación: %s\n"
+
+msgid ""
+"snappy autopilot triggered a reboot to boot into an up to date system -- "
+"temprorarily disable the reboot by running 'sudo shutdown -c'"
+msgstr ""
+"snappy autopilot provocó un reinicio para arrancar en un sistema actualizado "
+"-- desactive temporalmente el reinicio ejecutando «sudo shutdown -c»"
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to disable %s's service %s: %v"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to enable %s's service %s: %v"
+msgstr ""
+
+#, c-format
+msgid "unable to get logs: %v"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to start %s's service %s: %v"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to stop %s's service %s: %v"
+msgstr ""
+
+#. TRANSLATORS: the %s is a date
+#, c-format
+msgid "updated: %s\n"
+msgstr "actualizado: %s\n"
+
+#. TRANSLATORS: the %s is a version string
+#, c-format
+msgid "version: %s\n"
+msgstr "versión: %s\n"
--- /dev/null
+# Galician translation for snappy
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the snappy package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: snappy\n"
+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
+"POT-Creation-Date: 2015-10-15 15:53+0200\n"
+"PO-Revision-Date: 2015-10-21 16:12+0000\n"
+"Last-Translator: Marcos Lans <Unknown>\n"
+"Language-Team: Galician <gl@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2015-10-22 05:57+0000\n"
+"X-Generator: Launchpad (build 17812)\n"
+
+#. TRANSLATORS: the %s is a pkgname, the second a comma separated list of paths
+#, c-format
+msgid "%s: %s\n"
+msgstr "%s: %s\n"
+
+#. TRANSLATORS: the %s stand for "name", "version", "description"
+#, c-format
+msgid "%s\t%s\t%s (forks not shown: %d)\t"
+msgstr "%s\t%s\t%s (bifurcacións non mostradas: %d)\t"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' is no longer allowed to access '%s'\n"
+msgstr "«%s» xa non ten permiso de acceso a «%s»\n"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' is now allowed to access '%s'\n"
+msgstr "«%s» ten permiso de acceso a «%s»\n"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' previously allowed access to '%s'. Skipping\n"
+msgstr "«%s» permitiu o acceso previamente a «%s ». Saltando\n"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "'%s:' is not allowed to access additional hardware\n"
+msgstr "«%s:» non ten permiso de acceso a hardware adicional\n"
+
+msgid "(deprecated) please use \"list\""
+msgstr "(obsoleto) use «list»"
+
+msgid "2fa code: "
+msgstr "Código 2fa: "
+
+msgid ""
+"A concise summary of key attributes of the snappy system, such as the "
+"release and channel.\n"
+"\n"
+"The verbose output includes the specific version information for the factory "
+"image, the running image and the image that will be run on reboot, together "
+"with a list of the available channels for this image.\n"
+"\n"
+"Providing a package name will display information about a specific installed "
+"package.\n"
+"\n"
+"The verbose version of the info command for a package will also tell you the "
+"available channels for that package, when it was installed for the first "
+"time, disk space utilization, and in the case of frameworks, which apps are "
+"able to use the framework."
+msgstr ""
+"Resumo conciso dos atributos fundamentais do sistema snappy, como a "
+"publicación e a canle.\n"
+"\n"
+"A saída con información detallada inclúe a información específica da versión "
+"para a imaxe de fábrica, a imaxe en execución e a imaxe que se executará ao "
+"reiniciar, xunto cunha lista de canles para esta imaxe.\n"
+"\n"
+"Indicar un nome de paquete mostrará información sobre o paquete instalado "
+"específico.\n"
+"\n"
+"A versión con información detallada da orde para o paquete tamén mostrará as "
+"canles dispoñíbeis dese paquete, cando se instalou por primeira vez, a "
+"utilización de espazo no disco e no caso de frameworks, que aplicativos usa "
+"o framework."
+
+msgid "Activate a package"
+msgstr "Activar un paquete"
+
+msgid ""
+"Activate a package that has previously been deactivated. If the package is "
+"already activated, do nothing."
+msgstr ""
+"Activar un paquete previamente desactivado. Se o paquete xa está activado, "
+"ignorar."
+
+msgid ""
+"Allows rollback of a snap to a previous installed version. Without any "
+"arguments, the previous installed version is selected. It is also possible "
+"to specify the version to rollback to as a additional argument.\n"
+msgstr ""
+"Permite a reversión dun snap a unha versión instalada anteriormente. Se non "
+"se usan argumentos seleccionarase a versión instalada previamente. Tamén se "
+"pode especificar a versión á que reverter como un argumento adicional.\n"
+
+msgid "Assign a hardware device to a package"
+msgstr "Asignar un dispositivo de hardware a un paquete"
+
+msgid "Assign hardware to a specific installed package"
+msgstr "Asignar hardware a un paquete instalado"
+
+msgid "Builds a snap package"
+msgstr "Constrúe un paquete snap"
+
+#. TRANSLATORS: the first %q is the file that can not be read and %v is the error message
+#, c-format
+msgid "Can't read hook file %q: %v"
+msgstr "Non é posíbel ler o ficheiro do «hook» %q: %v"
+
+msgid ""
+"Configures a package. The configuration is a YAML file, provided in the "
+"specified file which can be \"-\" for stdin. Output of the command is the "
+"current configuration, so running this command with no input file provides a "
+"snapshot of the app's current config."
+msgstr ""
+"Configura un paquete. A configuración é un ficheiro YAML, proporcionado no "
+"ficheiro específico que pode ser «-» para stdin. A saída da orde é a "
+"configuración actual, de xeito que se se executa esta orde sen indicar un "
+"ficheiro de entrada proporcionará unha imaxe da configuración actual do "
+"aplicativo."
+
+msgid "Creates a snap package and if available, runs the review scripts."
+msgstr ""
+"Crea un paquete snap e, se están dispoñíbeis, executa os scripts de revisión."
+
+msgid "Deactivate a package"
+msgstr "Desactivar un paquete"
+
+msgid ""
+"Deactivate a package. If the package is already deactivated, do nothing."
+msgstr ""
+"Desactivar un paquete previamente activado. Se o paquete xa está "
+"desactivado, ignorar."
+
+msgid "Display a summary of key attributes of the snappy system."
+msgstr "Mostra un resumo dos atributos principais do sistema snappy."
+
+msgid "Do not clean up old versions of the package."
+msgstr "Non eliminar as versións antigas dos paquetes."
+
+msgid "Ensures system is running with latest parts"
+msgstr "Asegúrase de que o sistema está en execución coa últimas partes."
+
+msgid "First boot has already run"
+msgstr "Xa se executou o primeiro arranque"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Generated '%s' snap\n"
+msgstr "Xerado o paquete snap de «%s»\n"
+
+msgid "Include information about packages from the snappy store"
+msgstr "Incluír información sobre paquetes da tenda de snappy"
+
+msgid "Install a snap package"
+msgstr "Instalar un paquete snap"
+
+msgid "Install snaps even if the signature can not be verified."
+msgstr "Instalar snaps incluso con sinaturas non comprobadas."
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Installing %s\n"
+msgstr "Instalando %s\n"
+
+msgid "List active components installed on a snappy system"
+msgstr "Lista os compoñentes activos instalados nun sistema snappy"
+
+msgid "List assigned hardware device for a package"
+msgstr "Lista o dispositivo de hardware asignado a un paquete"
+
+msgid "List assigned hardware for a specific installed package"
+msgstr "Listar o hardware asignado a un paquete instalado"
+
+msgid "Log into the store"
+msgstr "Iniciar sesión na tenda"
+
+msgid "Login successful"
+msgstr "Inicio de sesión correcto"
+
+msgid "Name\tDate\tVersion\t"
+msgstr "Nome\tData\tVersión\t"
+
+msgid "Name\tDate\tVersion\tDeveloper\t"
+msgstr "Nome\tData\tVersión\tDesenvolvedor\t"
+
+msgid "Name\tVersion\tSummary\t"
+msgstr "Nome\tVersión\tResumo\t"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "No snap: '%s' found"
+msgstr "Ningún snap: atopouse «%s»"
+
+msgid "Password: "
+msgstr "Contrasinal: "
+
+msgid "Provide information about a specific installed package"
+msgstr "Proporcionar información sobre un paquete instalado"
+
+msgid ""
+"Provides a list of all active components installed on a snappy system.\n"
+"\n"
+"If requested, the command will find out if there are updates for any of the "
+"components and indicate that by appending a * to the date. This will be "
+"slower as it requires a round trip to the app store on the network.\n"
+"\n"
+"The developer information refers to non-mainline versions of a package (much "
+"like PPAs in deb-based Ubuntu). If the package is the primary version of "
+"that package in Ubuntu then the developer info is not shown. This allows one "
+"to identify packages which have custom, non-standard versions installed. As "
+"a special case, the \"sideload\" developer refers to packages installed "
+"manually on the system.\n"
+"\n"
+"When a verbose listing is requested, information about the channel used is "
+"displayed; which is one of alpha, beta, rc or stable, and all fields are "
+"fully expanded too. In some cases, older (inactive) versions of snappy "
+"packages will be installed, these will be shown in the verbose output and "
+"the active version indicated with a * appended to the name of the component."
+msgstr ""
+"Proporciona unha lista dos compoñentes activos instalados nun sistema "
+"snappy.\n"
+"\n"
+"Se se solicita, unha orde averiguará se hai actualizacións para algún dos "
+"compoñentes e indicarao anexando un * á data. Isto pode ser lento xa que "
+"require un proceso de ida e volta á tenda de aplicativos na rede.\n"
+"\n"
+"A información do desenvolvedor refírese a versións que non son da liña "
+"principal dun paquete (como as PPAs de Ubuntu baseado en Debian). Se o "
+"paquete é unha primeira versión do paquete en Ubuntu non se mostrará a "
+"información do desenvolvedor. Isto permite identificar paquetes con versións "
+"personalizadas non estándar instaladas. Como un caso especial, o "
+"desenvolvedor «sideload» refírese a paquetes instalados manualmente no "
+"sistema.\n"
+"\n"
+"Cando se solicite unha lista detallada, mostrarase a información sobre a "
+"canle utilizada; que pode ser alfa, beta, rc ou estábel e todos os campos "
+"estarán expandidos. Nalgúns casos, instalaranse versións antigas (inactivas) "
+"dos paquetes snappy; mostraranse na saída de información detallada e a "
+"versión activa indicarase cun * anexado ao nome do compoñente."
+
+msgid "Provides more detailed information"
+msgstr "Proporciona unha información máis detallada"
+
+msgid "Purge an installed package."
+msgstr "Purga un paquete instalado."
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Purging %s\n"
+msgstr "Purgando %s\n"
+
+msgid "Query and modify snappy services"
+msgstr "Consultar e modificar sevizos snappy"
+
+msgid "Query and modify snappy services of locally-installed packages"
+msgstr ""
+"Consultar e modificar sevizos snappy de paquetes instalados localmente"
+
+msgid "Query the store for available packages"
+msgstr "Consulta na tenda os paquetes dispoñíbeis"
+
+msgid "Reboot if necessary to be on the latest running system."
+msgstr "Reiniciar se é necesario estar no último sistema en execución."
+
+#. TRANSLATORS: the first %s is a pkgname the second a version
+#, c-format
+msgid "Reboot to use %s version %s."
+msgstr "Reiniciar para usar %s na versión %s."
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Reboot to use the new %s."
+msgstr "Reiniciar para usar o novo %s."
+
+#. TRANSLATORS: the %s shows a comma separated list
+#. of package names
+#, c-format
+msgid "Rebooting to satisfy updates for %s\n"
+msgstr "Reiniciando para rematar as actualizacións de %s\n"
+
+msgid "Remove a snapp part"
+msgstr "Retirar unha parte snapp"
+
+msgid "Remove all the data from the listed packages"
+msgstr "Eliminar todos os datos dos paquetes da lista"
+
+msgid ""
+"Remove all the data from the listed packages. Normally this is used for "
+"packages that have been removed and attempting to purge data for an "
+"installed package will result in an error. The --installed option overrides "
+"that and enables the administrator to purge all data for an installed "
+"package (effectively resetting the package completely)."
+msgstr ""
+"Eliminar todos os datos dos paquetes da lista. Normalmente úsase para "
+"paquetes eliminados que cando se tentan purgar os seus datos obtemos un "
+"erro. A opción --installed anula iso e activa que o aministrador poida "
+"purgar todos os datos dun paquete instalado (restabelecendo eficaz e "
+"completamente o paquete)"
+
+msgid "Remove hardware from a specific installed package"
+msgstr "Eliminar hardware para un paquete instalado"
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Removing %s\n"
+msgstr "Eliminando %s\n"
+
+msgid "Rollback to a previous version of a package"
+msgstr "Reverter a unha versión anterior dun paquete"
+
+msgid "Search for packages to install"
+msgstr "Buscar paquetes para instalar"
+
+msgid "Set configuration for a specific installed package"
+msgstr "Configurar un paquete instalado"
+
+msgid "Set configuration for an installed package."
+msgstr "Estabelecer a configuración dun paquete instalado."
+
+msgid "Set properties of system or package"
+msgstr "Estabelecer as propiedades do sistema ou paquete"
+
+msgid ""
+"Set properties of system or package\n"
+"\n"
+"Supported properties are:\n"
+" active=VERSION\n"
+"\n"
+"Example:\n"
+" set hello-world active=1.0\n"
+msgstr ""
+"Estabelece as propiedade do sistema ou paquete\n"
+"\n"
+"As propiedades aceptadas son:\n"
+" active=VERSION\n"
+"\n"
+"Exemplo:\n"
+" set hello-world active=1.0\n"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is the new version
+#, c-format
+msgid "Setting %s to version %s\n"
+msgstr "Axustando %s á versión %s\n"
+
+msgid "Show all available forks of a package"
+msgstr "Mostrar todas as bifurcacións dispoñíbeis dun paquete"
+
+msgid "Show available updates (requires network)"
+msgstr "Mostrar as actualizacións dispoñíbeis (precisa conexión á rede)"
+
+msgid "Show channel information and expand all fields"
+msgstr "Mostrar a información da canle e ampliar todos os campos"
+
+msgid "Snap\tService\tState"
+msgstr "Snap\tServizo\tEstado"
+
+msgid "Specify an alternate output directory for the resulting package"
+msgstr "Indicar un cartafol de saída alternativo para o paquete resultante"
+
+msgid "The Package to install (name or path)"
+msgstr "Paquete a instalar (nome ou ruta)"
+
+msgid ""
+"The \"versions\" command is no longer available.\n"
+"\n"
+"Please use the \"list\" command instead to see what is installed.\n"
+"The \"list -u\" (or \"list --updates\") will show you the available updates\n"
+"and \"list -v\" (or \"list --verbose\") will show all installed versions.\n"
+msgstr ""
+"A orde «versions» xa non está dispoñíbel.\n"
+"\n"
+"Use a orde «list» no seu canto para ver cal está instalada.\n"
+"«list -u» (ou «list --updates») mostrará as actualizacións dispoñíbeis.\n"
+"e «list -v» (ou «list --verbose») mostrará todas as versións instaladas.\n"
+
+msgid "The configuration for the given file"
+msgstr "Configuración para o ficheiro dado"
+
+msgid "The configuration for the given install"
+msgstr "Configuración para a instalación dada"
+
+msgid "The hardware device path (e.g. /dev/ttyUSB0)"
+msgstr "Ruta do dispositivo de hardware (p.e. /dev/ttyUSB0)"
+
+msgid "The package to rollback "
+msgstr "Paquete que reverter "
+
+msgid "The version to rollback to"
+msgstr "Versión á que reverter"
+
+msgid ""
+"This command adds access to a specific hardware device (e.g. /dev/ttyUSB0) "
+"for an installed package."
+msgstr ""
+"Esta orde engade o acceso a un dispositivo de hardware específico para un "
+"paquete instalado (p.e. /dev/ttyUSB0)."
+
+msgid "This command is no longer available, please use the \"list\" command"
+msgstr "Esta orde xa non está dispoñíbel, use a orde «list»"
+
+msgid "This command list what hardware an installed package can access"
+msgstr "Esta orde lista o hardware ao que pode acceder un paquete instalado"
+
+msgid "This command logs the given username into the store"
+msgstr "Esta orde rexistra o nome de usuario dado na tenda"
+
+msgid ""
+"This command removes access of a specific hardware device (e.g. "
+"/dev/ttyUSB0) for an installed package."
+msgstr ""
+"Esta orde elimina o acceso a un dispositivo específico de hardware (p.e "
+"/dev/ttyUSB0) dun paquete instalado."
+
+msgid "Unassign a hardware device to a package"
+msgstr "Desligar un dispositivo de hardware dun paquete"
+
+msgid "Update all installed parts"
+msgstr "Actualizar todas as partes instaladas"
+
+msgid "Use --show-all to see all available forks."
+msgstr "Usar --show-all para ver as bifurcacións dispoñíbeis."
+
+msgid "Username for the login"
+msgstr "Nome de usuario para o acceso"
+
+#. TRANSLATORS: the %s represents a list of installed appnames
+#. (e.g. "apps: foo, bar, baz")
+#, c-format
+msgid "apps: %s\n"
+msgstr "aplicativos: %s\n"
+
+#. TRANSLATORS: the %s an architecture string
+#, c-format
+msgid "architecture: %s\n"
+msgstr "arquitectura: %s\n"
+
+#. TRANSLATORS: the %s is a size
+#, c-format
+msgid "binary-size: %v\n"
+msgstr "tamaño do binario: %v\n"
+
+#. TRANSLATORS: the %s is a channel name
+#, c-format
+msgid "channel: %s\n"
+msgstr "canle: %s\n"
+
+#. TRANSLATORS: the %s is a size
+#, c-format
+msgid "data-size: %s\n"
+msgstr "data-tamaño: %s\n"
+
+#. TRANSLATORS: the %s is a comma separated list of framework names
+#, c-format
+msgid "frameworks: %s\n"
+msgstr "contornos de traballo: %s\n"
+
+#. TRANSLATORS: the %s is a date
+#, c-format
+msgid "installed: %s\n"
+msgstr "instalado: %s\n"
+
+msgid "package name is required"
+msgstr "precísase o nome do paquete"
+
+msgid "produces manpage"
+msgstr "produce unha manpage"
+
+#. TRANSLATORS: the %s release string
+#, c-format
+msgid "release: %s\n"
+msgstr "publicación: %s\n"
+
+msgid ""
+"snappy autopilot triggered a reboot to boot into an up to date system -- "
+"temprorarily disable the reboot by running 'sudo shutdown -c'"
+msgstr ""
+"snappy autopilot activou un reinicio para comezar nun sistema actualizado -- "
+"desactive temporalmente o reinicio executando «sudo shutdown -c»"
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to disable %s's service %s: %v"
+msgstr "Non foi posíbel desactivar o servizo de %s %s: %v"
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to enable %s's service %s: %v"
+msgstr "non foi posíbel activar o servizo para %s %s: %v"
+
+#, c-format
+msgid "unable to get logs: %v"
+msgstr "non foi posíbel obter os rexistros: %v"
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to start %s's service %s: %v"
+msgstr "non foi posíbel iniciar o servizo para %s %s: %v"
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to stop %s's service %s: %v"
+msgstr "non foi posíbel parar o servizo para %s %s: %v"
+
+#. TRANSLATORS: the %s is a date
+#, c-format
+msgid "updated: %s\n"
+msgstr "actualizado: %s\n"
+
+#. TRANSLATORS: the %s is a version string
+#, c-format
+msgid "version: %s\n"
+msgstr "versión: %s\n"
--- /dev/null
+# Uyghur translation for snappy
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the snappy package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: snappy\n"
+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
+"POT-Creation-Date: 2015-10-15 15:53+0200\n"
+"PO-Revision-Date: 2015-10-26 03:56+0000\n"
+"Last-Translator: Eltikin <eltikinuyghur@hotmail.com>\n"
+"Language-Team: Uyghur <ug@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2015-10-26 05:27+0000\n"
+"X-Generator: Launchpad (build 17812)\n"
+
+#. TRANSLATORS: the %s is a pkgname, the second a comma separated list of paths
+#, c-format
+msgid "%s: %s\n"
+msgstr "%s: %s\n"
+
+#. TRANSLATORS: the %s stand for "name", "version", "description"
+#, c-format
+msgid "%s\t%s\t%s (forks not shown: %d)\t"
+msgstr "%s\t%s\t%s (forks not shown: %d)\t"
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' is no longer allowed to access '%s'\n"
+msgstr ""
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' is now allowed to access '%s'\n"
+msgstr ""
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is a path
+#, c-format
+msgid "'%s' previously allowed access to '%s'. Skipping\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "'%s:' is not allowed to access additional hardware\n"
+msgstr ""
+
+msgid "(deprecated) please use \"list\""
+msgstr ""
+
+msgid "2fa code: "
+msgstr ""
+
+msgid ""
+"A concise summary of key attributes of the snappy system, such as the "
+"release and channel.\n"
+"\n"
+"The verbose output includes the specific version information for the factory "
+"image, the running image and the image that will be run on reboot, together "
+"with a list of the available channels for this image.\n"
+"\n"
+"Providing a package name will display information about a specific installed "
+"package.\n"
+"\n"
+"The verbose version of the info command for a package will also tell you the "
+"available channels for that package, when it was installed for the first "
+"time, disk space utilization, and in the case of frameworks, which apps are "
+"able to use the framework."
+msgstr ""
+
+msgid "Activate a package"
+msgstr ""
+
+msgid ""
+"Activate a package that has previously been deactivated. If the package is "
+"already activated, do nothing."
+msgstr ""
+
+msgid ""
+"Allows rollback of a snap to a previous installed version. Without any "
+"arguments, the previous installed version is selected. It is also possible "
+"to specify the version to rollback to as a additional argument.\n"
+msgstr ""
+
+msgid "Assign a hardware device to a package"
+msgstr ""
+
+msgid "Assign hardware to a specific installed package"
+msgstr ""
+
+msgid "Builds a snap package"
+msgstr ""
+
+#. TRANSLATORS: the first %q is the file that can not be read and %v is the error message
+#, c-format
+msgid "Can't read hook file %q: %v"
+msgstr ""
+
+msgid ""
+"Configures a package. The configuration is a YAML file, provided in the "
+"specified file which can be \"-\" for stdin. Output of the command is the "
+"current configuration, so running this command with no input file provides a "
+"snapshot of the app's current config."
+msgstr ""
+
+msgid "Creates a snap package and if available, runs the review scripts."
+msgstr ""
+
+msgid "Deactivate a package"
+msgstr ""
+
+msgid ""
+"Deactivate a package. If the package is already deactivated, do nothing."
+msgstr ""
+
+msgid "Display a summary of key attributes of the snappy system."
+msgstr ""
+
+msgid "Do not clean up old versions of the package."
+msgstr ""
+
+msgid "Ensures system is running with latest parts"
+msgstr ""
+
+msgid "First boot has already run"
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Generated '%s' snap\n"
+msgstr ""
+
+msgid "Include information about packages from the snappy store"
+msgstr ""
+
+msgid "Install a snap package"
+msgstr ""
+
+msgid "Install snaps even if the signature can not be verified."
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Installing %s\n"
+msgstr "ئورنىتىۋاتىدۇ %s\n"
+
+msgid "List active components installed on a snappy system"
+msgstr ""
+
+msgid "List assigned hardware device for a package"
+msgstr ""
+
+msgid "List assigned hardware for a specific installed package"
+msgstr ""
+
+msgid "Log into the store"
+msgstr ""
+
+msgid "Login successful"
+msgstr ""
+
+msgid "Name\tDate\tVersion\t"
+msgstr ""
+
+msgid "Name\tDate\tVersion\tDeveloper\t"
+msgstr ""
+
+msgid "Name\tVersion\tSummary\t"
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "No snap: '%s' found"
+msgstr ""
+
+msgid "Password: "
+msgstr "ئىم: "
+
+msgid "Provide information about a specific installed package"
+msgstr ""
+
+msgid ""
+"Provides a list of all active components installed on a snappy system.\n"
+"\n"
+"If requested, the command will find out if there are updates for any of the "
+"components and indicate that by appending a * to the date. This will be "
+"slower as it requires a round trip to the app store on the network.\n"
+"\n"
+"The developer information refers to non-mainline versions of a package (much "
+"like PPAs in deb-based Ubuntu). If the package is the primary version of "
+"that package in Ubuntu then the developer info is not shown. This allows one "
+"to identify packages which have custom, non-standard versions installed. As "
+"a special case, the \"sideload\" developer refers to packages installed "
+"manually on the system.\n"
+"\n"
+"When a verbose listing is requested, information about the channel used is "
+"displayed; which is one of alpha, beta, rc or stable, and all fields are "
+"fully expanded too. In some cases, older (inactive) versions of snappy "
+"packages will be installed, these will be shown in the verbose output and "
+"the active version indicated with a * appended to the name of the component."
+msgstr ""
+
+msgid "Provides more detailed information"
+msgstr ""
+
+msgid "Purge an installed package."
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Purging %s\n"
+msgstr ""
+
+msgid "Query and modify snappy services"
+msgstr ""
+
+msgid "Query and modify snappy services of locally-installed packages"
+msgstr ""
+
+msgid "Query the store for available packages"
+msgstr ""
+
+msgid "Reboot if necessary to be on the latest running system."
+msgstr ""
+
+#. TRANSLATORS: the first %s is a pkgname the second a version
+#, c-format
+msgid "Reboot to use %s version %s."
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Reboot to use the new %s."
+msgstr ""
+
+#. TRANSLATORS: the %s shows a comma separated list
+#. of package names
+#, c-format
+msgid "Rebooting to satisfy updates for %s\n"
+msgstr ""
+
+msgid "Remove a snapp part"
+msgstr ""
+
+msgid "Remove all the data from the listed packages"
+msgstr ""
+
+msgid ""
+"Remove all the data from the listed packages. Normally this is used for "
+"packages that have been removed and attempting to purge data for an "
+"installed package will result in an error. The --installed option overrides "
+"that and enables the administrator to purge all data for an installed "
+"package (effectively resetting the package completely)."
+msgstr ""
+
+msgid "Remove hardware from a specific installed package"
+msgstr ""
+
+#. TRANSLATORS: the %s is a pkgname
+#, c-format
+msgid "Removing %s\n"
+msgstr "ئۆچۈرۈۋاتىدۇ %s\n"
+
+msgid "Rollback to a previous version of a package"
+msgstr ""
+
+msgid "Search for packages to install"
+msgstr ""
+
+msgid "Set configuration for a specific installed package"
+msgstr ""
+
+msgid "Set configuration for an installed package."
+msgstr ""
+
+msgid "Set properties of system or package"
+msgstr ""
+
+msgid ""
+"Set properties of system or package\n"
+"\n"
+"Supported properties are:\n"
+" active=VERSION\n"
+"\n"
+"Example:\n"
+" set hello-world active=1.0\n"
+msgstr ""
+
+#. TRANSLATORS: the first %s is a pkgname, the second %s is the new version
+#, c-format
+msgid "Setting %s to version %s\n"
+msgstr ""
+
+msgid "Show all available forks of a package"
+msgstr ""
+
+msgid "Show available updates (requires network)"
+msgstr ""
+
+msgid "Show channel information and expand all fields"
+msgstr ""
+
+msgid "Snap\tService\tState"
+msgstr ""
+
+msgid "Specify an alternate output directory for the resulting package"
+msgstr ""
+
+msgid "The Package to install (name or path)"
+msgstr ""
+
+msgid ""
+"The \"versions\" command is no longer available.\n"
+"\n"
+"Please use the \"list\" command instead to see what is installed.\n"
+"The \"list -u\" (or \"list --updates\") will show you the available updates\n"
+"and \"list -v\" (or \"list --verbose\") will show all installed versions.\n"
+msgstr ""
+
+msgid "The configuration for the given file"
+msgstr ""
+
+msgid "The configuration for the given install"
+msgstr ""
+
+msgid "The hardware device path (e.g. /dev/ttyUSB0)"
+msgstr ""
+
+msgid "The package to rollback "
+msgstr ""
+
+msgid "The version to rollback to"
+msgstr ""
+
+msgid ""
+"This command adds access to a specific hardware device (e.g. /dev/ttyUSB0) "
+"for an installed package."
+msgstr ""
+
+msgid "This command is no longer available, please use the \"list\" command"
+msgstr ""
+
+msgid "This command list what hardware an installed package can access"
+msgstr ""
+
+msgid "This command logs the given username into the store"
+msgstr ""
+
+msgid ""
+"This command removes access of a specific hardware device (e.g. "
+"/dev/ttyUSB0) for an installed package."
+msgstr ""
+
+msgid "Unassign a hardware device to a package"
+msgstr ""
+
+msgid "Update all installed parts"
+msgstr ""
+
+msgid "Use --show-all to see all available forks."
+msgstr ""
+
+msgid "Username for the login"
+msgstr ""
+
+#. TRANSLATORS: the %s represents a list of installed appnames
+#. (e.g. "apps: foo, bar, baz")
+#, c-format
+msgid "apps: %s\n"
+msgstr "ئەپلەر %s\n"
+
+#. TRANSLATORS: the %s an architecture string
+#, c-format
+msgid "architecture: %s\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a size
+#, c-format
+msgid "binary-size: %v\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a channel name
+#, c-format
+msgid "channel: %s\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a size
+#, c-format
+msgid "data-size: %s\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a comma separated list of framework names
+#, c-format
+msgid "frameworks: %s\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a date
+#, c-format
+msgid "installed: %s\n"
+msgstr ""
+
+msgid "package name is required"
+msgstr ""
+
+msgid "produces manpage"
+msgstr ""
+
+#. TRANSLATORS: the %s release string
+#, c-format
+msgid "release: %s\n"
+msgstr ""
+
+msgid ""
+"snappy autopilot triggered a reboot to boot into an up to date system -- "
+"temprorarily disable the reboot by running 'sudo shutdown -c'"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to disable %s's service %s: %v"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to enable %s's service %s: %v"
+msgstr ""
+
+#, c-format
+msgid "unable to get logs: %v"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to start %s's service %s: %v"
+msgstr ""
+
+#. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
+#, c-format
+msgid "unable to stop %s's service %s: %v"
+msgstr ""
+
+#. TRANSLATORS: the %s is a date
+#, c-format
+msgid "updated: %s\n"
+msgstr ""
+
+#. TRANSLATORS: the %s is a version string
+#, c-format
+msgid "version: %s\n"
+msgstr ""
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package progress
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "unicode"
+
+ "github.com/cheggaaa/pb"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+// Meter is an interface to show progress to the user
+type Meter interface {
+ // Start progress with max "total" steps
+ Start(label string, total float64)
+
+ // set progress to the "current" step
+ Set(current float64)
+
+ // set "total" steps needed
+ SetTotal(total float64)
+
+ // Finish the progress display
+ Finished()
+
+ // Indicate indefinite activity by showing a spinner
+ Spin(msg string)
+
+ // interface for writer
+ Write(p []byte) (n int, err error)
+
+ // notify the user of miscellaneous events
+ Notify(string)
+}
+
+// NullProgress is a Meter that does nothing
+type NullProgress struct {
+}
+
+// Start does nothing
+func (t *NullProgress) Start(label string, total float64) {
+}
+
+// Set does nothing
+func (t *NullProgress) Set(current float64) {
+}
+
+// SetTotal does nothing
+func (t *NullProgress) SetTotal(total float64) {
+}
+
+// Finished does nothing
+func (t *NullProgress) Finished() {
+}
+
+// Write does nothing
+func (t *NullProgress) Write(p []byte) (n int, err error) {
+ return len(p), nil
+}
+
+// Notify does nothing
+func (t *NullProgress) Notify(string) {}
+
+// Spin does nothing
+func (t *NullProgress) Spin(msg string) {
+}
+
+const clearUntilEOL = "\033[K"
+
+// TextProgress show progress on the terminal
+type TextProgress struct {
+ Meter
+ pbar *pb.ProgressBar
+ spinStep int
+}
+
+// NewTextProgress returns a new TextProgress type
+func NewTextProgress() *TextProgress {
+ return &TextProgress{}
+}
+
+// Start starts showing progress
+func (t *TextProgress) Start(label string, total float64) {
+ t.pbar = pb.New64(int64(total))
+ t.pbar.ShowSpeed = true
+ t.pbar.Units = pb.U_BYTES
+ t.pbar.Prefix(label)
+ t.pbar.Start()
+}
+
+// Set sets the progress to the current value
+func (t *TextProgress) Set(current float64) {
+ t.pbar.Set(int(current))
+}
+
+// SetTotal set the total steps needed
+func (t *TextProgress) SetTotal(total float64) {
+ t.pbar.Total = int64(total)
+}
+
+// Finished stops displaying the progress
+func (t *TextProgress) Finished() {
+ if t.pbar != nil {
+ // workaround silly pb that always does a fmt.Println() on
+ // finish (unless NotPrint is set)
+ t.pbar.NotPrint = true
+ t.pbar.Finish()
+ t.pbar.NotPrint = false
+ }
+ fmt.Printf("\r\033[K")
+}
+
+// Write is there so that progress can implment a Writer and can be
+// used to display progress of io operations
+func (t *TextProgress) Write(p []byte) (n int, err error) {
+ return t.pbar.Write(p)
+}
+
+// Spin advances a spinner, i.e. can be used to show progress for operations
+// that have a unknown duration
+func (t *TextProgress) Spin(msg string) {
+ states := `|/-\`
+
+ // clear until end of line
+ fmt.Printf("\r[%c] %s%s", states[t.spinStep], msg, clearUntilEOL)
+ t.spinStep++
+ if t.spinStep >= len(states) {
+ t.spinStep = 0
+ }
+}
+
+// Agreed asks the user whether they agree to the given license text
+func (t *TextProgress) Agreed(intro, license string) bool {
+ if _, err := fmt.Println(intro); err != nil {
+ return false
+ }
+
+ // XXX: send it through a pager instead of this ugly thing
+ if _, err := fmt.Println(license); err != nil {
+ return false
+ }
+
+ reader := bufio.NewReader(os.Stdin)
+ if _, err := fmt.Print("Do you agree? [y/n] "); err != nil {
+ return false
+ }
+ r, _, err := reader.ReadRune()
+ if err != nil {
+ return false
+ }
+
+ return unicode.ToLower(r) == 'y'
+}
+
+// Notify the user of miscellaneous events
+func (*TextProgress) Notify(msg string) {
+ fmt.Printf("\r%s%s\n", msg, clearUntilEOL)
+}
+
+// MakeProgressBar creates an appropriate progress (which may be a
+// NullProgress bar if there is no associated terminal).
+func MakeProgressBar() Meter {
+ var pbar Meter
+ if attachedToTerminal() {
+ pbar = NewTextProgress()
+ } else {
+ pbar = &NullProgress{}
+ }
+
+ return pbar
+}
+
+// attachedToTerminal returns true if the calling process is attached to
+// a terminal device.
+var attachedToTerminal = func() bool {
+ fd := int(os.Stdin.Fd())
+
+ return terminal.IsTerminal(fd)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package progress
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type ProgressTestSuite struct {
+ attachedToTerminalReturn bool
+
+ originalAttachedToTerminal func() bool
+}
+
+var _ = Suite(&ProgressTestSuite{})
+
+func (ts *ProgressTestSuite) MockAttachedToTerminal() bool {
+ return ts.attachedToTerminalReturn
+}
+
+func (ts *ProgressTestSuite) TestSpin(c *C) {
+ f, err := ioutil.TempFile("", "progress-")
+ c.Assert(err, IsNil)
+ defer os.Remove(f.Name())
+ oldStdout := os.Stdout
+ os.Stdout = f
+
+ t := NewTextProgress()
+ for i := 0; i < 6; i++ {
+ t.Spin("msg")
+ }
+
+ os.Stdout = oldStdout
+ f.Sync()
+ f.Seek(0, 0)
+ progress, err := ioutil.ReadAll(f)
+ c.Assert(err, IsNil)
+ c.Assert(string(progress), Equals, "\r[|] msg\x1b[K\r[/] msg\x1b[K\r[-] msg\x1b[K\r[\\] msg\x1b[K\r[|] msg\x1b[K\r[/] msg\x1b[K")
+}
+
+func (ts *ProgressTestSuite) testAgreed(answer string, value bool, c *C) {
+ fout, err := ioutil.TempFile("", "progress-out-")
+ c.Assert(err, IsNil)
+ oldStdout := os.Stdout
+ os.Stdout = fout
+ defer func() {
+ os.Stdout = oldStdout
+ os.Remove(fout.Name())
+ fout.Close()
+ }()
+
+ fin, err := ioutil.TempFile("", "progress-in-")
+ c.Assert(err, IsNil)
+ oldStdin := os.Stdin
+ os.Stdin = fin
+ defer func() {
+ os.Stdin = oldStdin
+ os.Remove(fin.Name())
+ fin.Close()
+ }()
+
+ _, err = fmt.Fprintln(fin, answer)
+ c.Assert(err, IsNil)
+ _, err = fin.Seek(0, 0)
+ c.Assert(err, IsNil)
+
+ license := "Void where empty."
+
+ t := NewTextProgress()
+ c.Check(t.Agreed("blah blah", license), Equals, value)
+
+ _, err = fout.Seek(0, 0)
+ c.Assert(err, IsNil)
+ out, err := ioutil.ReadAll(fout)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "blah blah\n"+license+"\nDo you agree? [y/n] ")
+}
+
+func (ts *ProgressTestSuite) TestAgreed(c *C) {
+ ts.testAgreed("Y", true, c)
+ ts.testAgreed("N", false, c)
+}
+
+func (ts *ProgressTestSuite) TestNotify(c *C) {
+ fout, err := ioutil.TempFile("", "notify-out-")
+ c.Assert(err, IsNil)
+ oldStdout := os.Stdout
+ os.Stdout = fout
+ defer func() {
+ os.Stdout = oldStdout
+ os.Remove(fout.Name())
+ fout.Close()
+ }()
+
+ t := NewTextProgress()
+ t.Notify("blah blah")
+
+ _, err = fout.Seek(0, 0)
+ c.Assert(err, IsNil)
+ out, err := ioutil.ReadAll(fout)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, fmt.Sprintf("\rblah blah%s\n", clearUntilEOL))
+}
+
+func (ts *ProgressTestSuite) TestMakeProgressBar(c *C) {
+ var pbar Meter
+
+ ts.originalAttachedToTerminal = attachedToTerminal
+ attachedToTerminal = ts.MockAttachedToTerminal
+ defer func() {
+ // reset
+ attachedToTerminal = ts.originalAttachedToTerminal
+ }()
+
+ ts.attachedToTerminalReturn = true
+
+ pbar = MakeProgressBar()
+ c.Assert(pbar, FitsTypeOf, NewTextProgress())
+
+ ts.attachedToTerminalReturn = false
+
+ pbar = MakeProgressBar()
+ c.Assert(pbar, FitsTypeOf, &NullProgress{})
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package provisioning
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "time"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/partition"
+
+ "gopkg.in/yaml.v2"
+)
+
+const (
+ // InstallYamlFile is the name of the file created by
+ // ubuntu-device-flash(1), created at system installation time,
+ // that contains metadata on the installation.
+ //
+ // XXX: Public for ubuntu-device-flash(1)
+ InstallYamlFile = "install.yaml"
+)
+
+var (
+ // simplify testing
+ findBootloader = partition.FindBootloader
+)
+
+// ErrNoInstallYaml is emitted when InstallYamlFile does not exist.
+type ErrNoInstallYaml struct {
+ origErr error
+}
+
+func (e *ErrNoInstallYaml) Error() string {
+ return fmt.Sprintf("failed to read provisioning data: %s", e.origErr)
+}
+
+// InstallMeta encapsulates the metadata for a system install.
+type InstallMeta struct {
+ Timestamp time.Time
+}
+
+// InstallTool encapsulates metadata on the tool used to create the
+// system image.
+type InstallTool struct {
+ Name string
+ Path string
+ Version string
+}
+
+// InstallOptions summarises the options used when creating the system image.
+type InstallOptions struct {
+ Size int64 `yaml:"size"`
+ SizeUnit string `yaml:"size-unit"`
+ Output string
+ Channel string
+ DevicePart string `yaml:"device-part,omitempty"`
+ Gadget string
+ OS string
+ Kernel string
+ DeveloperMode bool `yaml:"developer-mode,omitempty"`
+}
+
+// InstallYaml represents 'InstallYamlFile'
+//
+// XXX: Public for ubuntu-device-flash
+type InstallYaml struct {
+ InstallMeta `yaml:"meta"`
+ InstallTool `yaml:"tool"`
+ InstallOptions `yaml:"options"`
+}
+
+func parseInstallYaml(path string) (*InstallYaml, error) {
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, &ErrNoInstallYaml{origErr: err}
+ }
+
+ return parseInstallYamlData(data)
+}
+
+func parseInstallYamlData(yamlData []byte) (*InstallYaml, error) {
+ var i InstallYaml
+ err := yaml.Unmarshal(yamlData, &i)
+ if err != nil {
+ logger.Noticef("Cannot parse install.yaml %q", yamlData)
+ return nil, err
+ }
+
+ return &i, nil
+}
+
+// InDeveloperMode returns true if the image was build with --developer-mode
+func InDeveloperMode() bool {
+ // FIXME: this is a bit terrible, we really need a single
+ // bootloader dir like /boot or /boot/loader
+ // instead of having to query the partition code
+ bootloader, err := findBootloader()
+ if err != nil {
+ // can only happy on systems like ubuntu classic
+ // that are not full snappy systems
+ return false
+ }
+
+ file := filepath.Join(bootloader.Dir(), InstallYamlFile)
+ if !osutil.FileExists(file) {
+ // no idea
+ return false
+ }
+
+ InstallYaml, err := parseInstallYaml(file)
+ if err != nil {
+ // no idea
+ return false
+ }
+
+ return InstallYaml.InstallOptions.DeveloperMode
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package provisioning
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ "github.com/snapcore/snapd/boot/boottest"
+ "github.com/snapcore/snapd/partition"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up gocheck into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type ProvisioningTestSuite struct {
+ mockYamlFile string
+
+ bootloader *boottest.MockBootloader
+}
+
+var _ = Suite(&ProvisioningTestSuite{})
+
+var yamlData = `
+meta:
+ timestamp: 2015-04-20T14:15:39.013515821+01:00
+
+tool:
+ name: ubuntu-device-flash
+ path: /usr/bin/ubuntu-device-flash
+ version: ""
+
+options:
+ size: 3
+ size-unit: GB
+ output: /tmp/bbb.img
+ channel: ubuntu-core/devel-proposed
+ device-part: /some/path/file.tgz
+ developer-mode: true
+`
+
+var yamlDataNoDevicePart = `
+meta:
+ timestamp: 2015-04-20T14:15:39.013515821+01:00
+
+tool:
+ name: ubuntu-device-flash
+ path: /usr/bin/ubuntu-device-flash
+ version: ""
+
+options:
+ size: 3
+ size-unit: GB
+ output: /tmp/bbb.img
+ channel: ubuntu-core/devel-proposed
+ developer-mode: true
+`
+
+var garbageData = `Fooled you!?`
+
+func (ts *ProvisioningTestSuite) SetUpTest(c *C) {
+ ts.bootloader = boottest.NewMockBootloader("mock", c.MkDir())
+ ts.mockYamlFile = filepath.Join(ts.bootloader.Dir(), "install.yaml")
+
+ findBootloader = func() (partition.Bootloader, error) {
+ return ts.bootloader, nil
+ }
+}
+
+func (ts *ProvisioningTestSuite) TearDownTest(c *C) {
+ findBootloader = partition.FindBootloader
+}
+
+func (ts *ProvisioningTestSuite) TestParseInstallYaml(c *C) {
+
+ _, err := parseInstallYaml(ts.mockYamlFile)
+ c.Check(err, ErrorMatches, `failed to read provisioning data: open .*/install.yaml: no such file or directory`)
+
+ err = ioutil.WriteFile(ts.mockYamlFile, []byte(yamlData), 0750)
+ c.Check(err, IsNil)
+ _, err = parseInstallYaml(ts.mockYamlFile)
+ c.Check(err, IsNil)
+
+ err = ioutil.WriteFile(ts.mockYamlFile, []byte(yamlDataNoDevicePart), 0750)
+ c.Check(err, IsNil)
+ _, err = parseInstallYaml(ts.mockYamlFile)
+ c.Check(err, IsNil)
+
+ err = ioutil.WriteFile(ts.mockYamlFile, []byte(garbageData), 0750)
+ c.Check(err, IsNil)
+ _, err = parseInstallYaml(ts.mockYamlFile)
+ c.Check(err, Not(Equals), nil)
+}
+
+func (ts *ProvisioningTestSuite) TestParseInstallYamlData(c *C) {
+
+ _, err := parseInstallYamlData([]byte(""))
+ c.Check(err, IsNil)
+
+ _, err = parseInstallYamlData([]byte(yamlData))
+ c.Check(err, IsNil)
+
+ _, err = parseInstallYamlData([]byte(yamlDataNoDevicePart))
+ c.Check(err, IsNil)
+
+ _, err = parseInstallYamlData([]byte(garbageData))
+ c.Check(err, Not(Equals), nil)
+}
+
+func (ts *ProvisioningTestSuite) TestInDeveloperModeWithDevModeOn(c *C) {
+ err := ioutil.WriteFile(ts.mockYamlFile, []byte(`
+options:
+ developer-mode: true
+`), 0644)
+ c.Assert(err, IsNil)
+ c.Assert(InDeveloperMode(), Equals, true)
+}
+
+func (ts *ProvisioningTestSuite) TestInDeveloperModeWithDevModeOff(c *C) {
+ err := ioutil.WriteFile(ts.mockYamlFile, []byte(`
+options:
+ developer-mode: false
+`), 0644)
+ c.Assert(err, IsNil)
+ c.Assert(InDeveloperMode(), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package release
+
+var ReadOSRelease = readOSRelease
+
+func MockOSReleasePath(filename string) (restore func()) {
+ old := osReleasePath
+ oldFallback := fallbackOsReleasePath
+ osReleasePath = filename
+ fallbackOsReleasePath = filename
+ return func() {
+ osReleasePath = old
+ fallbackOsReleasePath = oldFallback
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package release
+
+import (
+ "bufio"
+ "os"
+ "strings"
+ "unicode"
+)
+
+// Series holds the Ubuntu Core series for snapd to use.
+var Series = "16"
+
+// OS contains information about the system extracted from /etc/os-release.
+type OS struct {
+ ID string `json:"id"`
+ VersionID string `json:"version-id,omitempty"`
+}
+
+// ForceDevMode returns true if the distribution doesn't implement required
+// security features for confinement and devmode is forced.
+func (os *OS) ForceDevMode() bool {
+ switch os.ID {
+ case "neon":
+ return false
+ case "ubuntu":
+ return false
+ case "ubuntu-core":
+ return false
+ case "elementary":
+ switch os.VersionID {
+ case "0.4":
+ return false
+ default:
+ return true
+ }
+ default:
+ // NOTE: Other distributions can move out of devmode by
+ // integrating with the interface security backends. This will
+ // be documented separately in the porting guide.
+ return true
+ }
+
+}
+
+var (
+ osReleasePath = "/etc/os-release"
+ fallbackOsReleasePath = "/usr/lib/os-release"
+)
+
+// readOSRelease returns the os-release information of the current system.
+func readOSRelease() OS {
+ // TODO: separate this out into its own thing maybe (if made more general)
+ osRelease := OS{
+ VersionID: "unknown",
+ // from os-release(5): If not set, defaults to "ID=linux".
+ ID: "linux",
+ }
+
+ f, err := os.Open(osReleasePath)
+ if err != nil {
+ // this fallback is as per os-release(5)
+ f, err = os.Open(fallbackOsReleasePath)
+ if err != nil {
+ return osRelease
+ }
+ }
+
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ ws := strings.SplitN(scanner.Text(), "=", 2)
+ if len(ws) < 2 {
+ continue
+ }
+
+ k := strings.TrimSpace(ws[0])
+ v := strings.TrimFunc(ws[1], func(r rune) bool { return r == '"' || r == '\'' || unicode.IsSpace(r) })
+ // XXX: should also unquote things as per os-release(5) but not needed yet in practice
+ switch k {
+ case "ID":
+ // ID should be “A lower-case string (no spaces or
+ // other characters outside of 0–9, a–z, ".", "_" and
+ // "-") identifying the operating system, excluding any
+ // version information and suitable for processing by
+ // scripts or usage in generated filenames.”
+ //
+ // So we mangle it a little bit to account for people
+ // not being too good at reading comprehension.
+ // Works around e.g. lp:1602317
+ osRelease.ID = strings.Fields(strings.ToLower(v))[0]
+ case "VERSION_ID":
+ osRelease.VersionID = v
+ }
+ }
+
+ return osRelease
+}
+
+// OnClassic states whether the process is running inside a
+// classic Ubuntu system or a native Ubuntu Core image.
+var OnClassic bool
+
+// ReleaseInfo contains data loaded from /etc/os-release on startup.
+var ReleaseInfo OS
+
+func init() {
+ ReleaseInfo = readOSRelease()
+
+ OnClassic = (ReleaseInfo.ID != "ubuntu-core")
+}
+
+// MockOnClassic forces the process to appear inside a classic
+// Ubuntu system or a native image for testing purposes.
+func MockOnClassic(onClassic bool) (restore func()) {
+ old := OnClassic
+ OnClassic = onClassic
+ return func() { OnClassic = old }
+}
+
+// MockReleaseInfo fakes a given information to appear in ReleaseInfo,
+// as if it was read /etc/os-release on startup.
+func MockReleaseInfo(osRelease *OS) (restore func()) {
+ old := ReleaseInfo
+ ReleaseInfo = *osRelease
+ return func() { ReleaseInfo = old }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package release_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/release"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type ReleaseTestSuite struct {
+}
+
+var _ = Suite(&ReleaseTestSuite{})
+
+func (s *ReleaseTestSuite) TestSetup(c *C) {
+ c.Check(release.Series, Equals, "16")
+}
+
+func mockOSRelease(c *C) string {
+ // FIXME: use AddCleanup here once available so that we
+ // can do release.SetLSBReleasePath() here directly
+ mockOSRelease := filepath.Join(c.MkDir(), "mock-os-release")
+ s := `
+NAME="Ubuntu"
+VERSION="18.09 (Awesome Artichoke)"
+ID=ubuntu
+ID_LIKE=debian
+PRETTY_NAME="I'm not real!"
+VERSION_ID="18.09"
+HOME_URL="http://www.ubuntu.com/"
+SUPPORT_URL="http://help.ubuntu.com/"
+BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
+`
+ err := ioutil.WriteFile(mockOSRelease, []byte(s), 0644)
+ c.Assert(err, IsNil)
+
+ return mockOSRelease
+}
+
+func (s *ReleaseTestSuite) TestReadOSRelease(c *C) {
+ reset := release.MockOSReleasePath(mockOSRelease(c))
+ defer reset()
+
+ os := release.ReadOSRelease()
+ c.Check(os.ID, Equals, "ubuntu")
+ c.Check(os.VersionID, Equals, "18.09")
+}
+
+func (s *ReleaseTestSuite) TestReadWonkyOSRelease(c *C) {
+ mockOSRelease := filepath.Join(c.MkDir(), "mock-os-release")
+ dump := `NAME="elementary OS"
+VERSION="0.4 Loki"
+ID="elementary OS"
+ID_LIKE=ubuntu
+PRETTY_NAME="elementary OS Loki"
+VERSION_ID="0.4"
+HOME_URL="http://elementary.io/"
+SUPPORT_URL="http://elementary.io/support/"
+BUG_REPORT_URL="https://bugs.launchpad.net/elementary/+filebug"`
+ err := ioutil.WriteFile(mockOSRelease, []byte(dump), 0644)
+ c.Assert(err, IsNil)
+
+ reset := release.MockOSReleasePath(mockOSRelease)
+ defer reset()
+
+ os := release.ReadOSRelease()
+ c.Check(os.ID, Equals, "elementary")
+ c.Check(os.VersionID, Equals, "0.4")
+}
+
+func (s *ReleaseTestSuite) TestReadOSReleaseNotFound(c *C) {
+ reset := release.MockOSReleasePath("not-there")
+ defer reset()
+
+ os := release.ReadOSRelease()
+ c.Assert(os, DeepEquals, release.OS{ID: "linux", VersionID: "unknown"})
+}
+
+func (s *ReleaseTestSuite) TestOnClassic(c *C) {
+ reset := release.MockOnClassic(true)
+ defer reset()
+ c.Assert(release.OnClassic, Equals, true)
+
+ reset = release.MockOnClassic(false)
+ defer reset()
+ c.Assert(release.OnClassic, Equals, false)
+}
+
+func (s *ReleaseTestSuite) TestReleaseInfo(c *C) {
+ reset := release.MockReleaseInfo(&release.OS{
+ ID: "distro-id",
+ })
+ defer reset()
+ c.Assert(release.ReleaseInfo.ID, Equals, "distro-id")
+}
+
+func (s *ReleaseTestSuite) TestForceDevMode(c *C) {
+ // Restore real OS info at the end of this function.
+ defer release.MockReleaseInfo(&release.OS{})()
+ distros := []struct {
+ id string
+ idVersion string
+ devmode bool
+ }{
+ // Please keep this list sorted
+ {id: "arch", devmode: true},
+ {id: "debian", devmode: true},
+ {id: "elementary", devmode: true},
+ {id: "elementary", idVersion: "0.4", devmode: false},
+ {id: "fedora", devmode: true},
+ {id: "gentoo", devmode: true},
+ {id: "neon", devmode: false},
+ {id: "opensuse", devmode: true},
+ {id: "rhel", devmode: true},
+ {id: "ubuntu", devmode: false},
+ {id: "ubuntu-core", devmode: false},
+ }
+ for _, distro := range distros {
+ rel := &release.OS{ID: distro.id, VersionID: distro.idVersion}
+ c.Logf("checking distribution %#v", rel)
+ release.MockReleaseInfo(rel)
+ c.Assert(release.ReleaseInfo.ForceDevMode(), Equals, distro.devmode)
+ }
+}
--- /dev/null
+#!/bin/sh
+export LANG=C.UTF-8
+export LANGUAGE=en
+set -eu
+
+if which goctest >/dev/null; then
+ goctest="goctest"
+else
+ goctest="go test"
+fi
+
+STATIC=
+UNIT=
+SPREAD=
+DEPRECATED=
+
+case "${1:-all}" in
+ all)
+ STATIC=1
+ UNIT=1
+ DEPRECATED=1
+ ;;
+ --static)
+ STATIC=1
+ ;;
+ --unit)
+ UNIT=1
+ ;;
+ --spread)
+ SPREAD=1
+ ;;
+ *)
+ echo "Wrong flag ${1}. To run a single suite use --static, --unit, --spread."
+ exit 1
+esac
+
+CURRENTTRAP="true"
+EXIT_CODE=99
+store_exit_code() {
+ EXIT_CODE=$?
+}
+exit_with_exit_code() {
+ exit $EXIT_CODE
+}
+addtrap() {
+ CURRENTTRAP="$CURRENTTRAP ; $1"
+ trap "store_exit_code; $CURRENTTRAP ; exit_with_exit_code" EXIT
+}
+
+endmsg() {
+ if [ $EXIT_CODE -eq 0 ]; then
+ p="success.txt"
+ m="All good, what could possibly go wrong."
+ else
+ p="failure.txt"
+ m="Crushing failure and despair."
+ fi
+ echo
+ if [ -t 1 -a -z "$STATIC" ]; then
+ cat "data/$p"
+ else
+ echo "$m"
+ fi
+}
+addtrap endmsg
+
+# Append the coverage profile of a package to the project coverage.
+append_coverage() {
+ local profile="$1"
+ if [ -f $profile ]; then
+ cat $profile | grep -v "mode: set" | cat >> .coverage/coverage.out
+ rm $profile
+ fi
+}
+
+if [ "$STATIC" = 1 ]; then
+ ./get-deps.sh
+
+ # Run static tests.
+ echo Checking docs
+ ./mdlint.py docs/*.md
+
+ echo Checking formatting
+ fmt=""
+ for pkg in $(go list ./... | grep -v '/vendor/' ); do
+ s="$(gofmt -s -l $GOPATH/src/$pkg)"
+ if [ -n "$s" ]; then
+ fmt="$s\n$fmt"
+ fi
+ done
+
+ if [ -n "$fmt" ]; then
+ echo "Formatting wrong in following files:"
+ echo "$fmt"
+ exit 1
+ fi
+
+ # go vet
+ echo Running vet
+ for pkg in $(go list ./... | grep -v '/vendor/' ); do
+ go vet $pkg
+ done
+
+ echo Checking spelling errors
+ go get -u github.com/client9/misspell/cmd/misspell
+ for file in $(ls . | grep -v 'vendor\|po'); do
+ ${GOBIN:-$GOPATH/bin}/misspell -error -i auther $file
+ done
+
+ echo Checking for ineffective assignments
+ go get -u github.com/gordonklaus/ineffassign
+ for file in $(ls . | grep -v 'vendor\|po'); do
+ ${GOBIN:-$GOPATH/bin}/ineffassign $file
+ done
+fi
+
+if [ "$UNIT" = 1 ]; then
+ ./get-deps.sh
+
+ # Prepare the coverage output profile.
+ rm -rf .coverage
+ mkdir .coverage
+ echo "mode: set" > .coverage/coverage.out
+
+ echo Building
+ go build -v github.com/snapcore/snapd/...
+
+ # tests
+ echo Running tests from $(pwd)
+ for pkg in $(go list ./... | grep -v '/vendor/' ); do
+ $goctest -v -coverprofile=.coverage/profile.out $pkg
+ append_coverage .coverage/profile.out
+ done
+
+fi
+
+if [ "$SPREAD" = 1 ]; then
+ TMP_SPREAD="$(mktemp -d)"
+ addtrap "rm -rf \"$TMP_SPREAD\""
+
+ export PATH=$TMP_SPREAD:$PATH
+ ( cd $TMP_SPREAD && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz )
+
+ spread -v linode:
+
+ # cleanup the debian-ubuntu-14.04
+ rm -rf debian-ubuntu-14.04
+fi
+
+if [ "$DEPRECATED" = 1 ]; then
+ ./get-deps.sh
+
+fi
+
+UNCLEAN="$(git status -s|grep ^??)" || true
+if [ -n "$UNCLEAN" ]; then
+ cat <<EOF
+
+There are files left in the git tree after the tests:
+
+$UNCLEAN
+EOF
+ exit 1
+fi
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+)
+
+func GuessAppsForBroken(info *Info) map[string]*AppInfo {
+ out := make(map[string]*AppInfo)
+
+ // guess binaries first
+ name := info.SuggestedName
+ for _, p := range []string{name, fmt.Sprintf("%s.*", name)} {
+ matches, _ := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, p))
+ for _, m := range matches {
+ l := strings.SplitN(filepath.Base(m), ".", 2)
+ var appname string
+ if len(l) == 1 {
+ appname = l[0]
+ } else {
+ appname = l[1]
+ }
+ out[appname] = &AppInfo{
+ Snap: info,
+ Name: appname,
+ }
+ }
+ }
+
+ // guess the services next
+ matches, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, fmt.Sprintf("snap.%s.*.service", name)))
+ for _, m := range matches {
+ appname := strings.Split(m, ".")[2]
+ out[appname] = &AppInfo{
+ Snap: info,
+ Name: appname,
+ Daemon: "simple",
+ }
+ }
+
+ return out
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+)
+
+type brokenSuite struct{}
+
+var _ = Suite(&brokenSuite{})
+
+func (s *brokenSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *brokenSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func touch(c *C, path string) {
+ err := os.MkdirAll(filepath.Dir(path), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(path, nil, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *brokenSuite) TestGuessAppsForBrokenBinaries(c *C) {
+ touch(c, filepath.Join(dirs.SnapBinariesDir, "foo"))
+ touch(c, filepath.Join(dirs.SnapBinariesDir, "foo.bar"))
+
+ info := &snap.Info{SuggestedName: "foo"}
+ apps := snap.GuessAppsForBroken(info)
+ c.Check(apps, HasLen, 2)
+ c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo"})
+ c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar"})
+}
+
+func (s *brokenSuite) TestGuessAppsForBrokenServices(c *C) {
+ touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo.foo.service"))
+ touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo.bar.service"))
+
+ info := &snap.Info{SuggestedName: "foo"}
+ apps := snap.GuessAppsForBroken(info)
+ c.Check(apps, HasLen, 2)
+ c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo", Daemon: "simple"})
+ c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar", Daemon: "simple"})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap/snapdir"
+ "github.com/snapcore/snapd/snap/squashfs"
+)
+
+// Container is the interface to interact with the low-level snap files
+type Container interface {
+ // Size returns the size of the snap in bytes.
+ Size() (int64, error)
+
+ // ReadFile returns the content of a single file from the snap.
+ ReadFile(relative string) ([]byte, error)
+
+ // ListDir returns the content of a single directory inside the snap.
+ ListDir(path string) ([]string, error)
+
+ // Install copies the snap file to targetPath (and possibly unpacks it to mountDir)
+ Install(targetPath, mountDir string) error
+
+ // Unpack unpacks the src parts to the dst directory
+ Unpack(src, dst string) error
+}
+
+// backend implements a specific snap format
+type snapFormat struct {
+ magic []byte
+ open func(fn string) (Container, error)
+}
+
+// formatHandlers is the registry of known formats, squashfs is the only one atm.
+var formatHandlers = []snapFormat{
+ {squashfs.Magic, func(p string) (Container, error) {
+ return squashfs.New(p), nil
+ }},
+}
+
+// Open opens a given snap file with the right backend
+func Open(path string) (Container, error) {
+
+ // see if it's a snapdir first
+ if osutil.FileExists(filepath.Join(path, "meta", "snap.yaml")) {
+ return snapdir.New(path), nil
+ }
+
+ // open the file and check magic
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("cannot open snap: %v", err)
+ }
+ defer f.Close()
+
+ header := make([]byte, 20)
+ if _, err := f.ReadAt(header, 0); err != nil {
+ return nil, fmt.Errorf("cannot read snap: %v", err)
+ }
+
+ for _, h := range formatHandlers {
+ if bytes.HasPrefix(header, h.magic) {
+ return h.open(path)
+ }
+ }
+
+ return nil, fmt.Errorf("cannot open snap: unknown header: %q", header)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snapdir"
+)
+
+type FileSuite struct{}
+
+var _ = Suite(&FileSuite{})
+
+func (s *FileSuite) TestFileOpenForSnapDir(c *C) {
+ sd := c.MkDir()
+ snapYaml := filepath.Join(sd, "meta", "snap.yaml")
+ err := os.MkdirAll(filepath.Dir(snapYaml), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(snapYaml, []byte(`name: foo`), 0644)
+ c.Assert(err, IsNil)
+
+ f, err := snap.Open(sd)
+ c.Assert(err, IsNil)
+ c.Assert(f, FitsTypeOf, &snapdir.SnapDir{})
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import "fmt"
+
+type AlreadyInstalledError struct {
+ Snap string
+}
+
+func (e AlreadyInstalledError) Error() string {
+ return fmt.Sprintf("snap %q is already installed", e.Snap)
+}
+
+type NotInstalledError struct {
+ Snap string
+ Rev Revision
+}
+
+func (e NotInstalledError) Error() string {
+ if e.Rev.Unset() {
+ return fmt.Sprintf("snap %q is not installed", e.Snap)
+ }
+ return fmt.Sprintf("revision %s of snap %q is not installed", e.Rev, e.Snap)
+}
+
+type NoUpdateAvailableError struct {
+ Snap string
+}
+
+func (e NoUpdateAvailableError) Error() string {
+ return fmt.Sprintf("snap %q has no updates available", e.Snap)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+var (
+ ImplicitSlotsForTests = implicitSlots
+ ImplicitClassicSlotsForTests = implicitClassicSlots
+ NewHookType = newHookType
+)
+
+func MockSupportedHookTypes(hookTypes []*HookType) (restore func()) {
+ old := supportedHooks
+ supportedHooks = hookTypes
+ return func() { supportedHooks = old }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+
+ "gopkg.in/yaml.v2"
+)
+
+type GadgetInfo struct {
+ Volumes map[string]GadgetVolume `yaml:"volumes,omitempty"`
+
+ // Default configuration for snaps (snap-id => key => value).
+ Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"`
+}
+
+type GadgetVolume struct {
+ Schema string `yaml:"schema"`
+ Bootloader string `yaml:"bootloader"`
+ ID string `yaml:"id"`
+ Structure []VolumeStructure `yaml:"structure"`
+}
+
+// TODO Offsets and sizes are strings to support unit suffixes.
+// Is that a good idea? *2^N or *10^N? We'll probably want a richer
+// type when we actually handle these.
+
+type VolumeStructure struct {
+ Label string `yaml:"label"`
+ Offset string `yaml:"offset"`
+ OffsetWrite string `yaml:"offset-write"`
+ Size string `yaml:"size"`
+ Type string `yaml:"type"`
+ ID string `yaml:"id"`
+ Filesystem string `yaml:"filesystem"`
+ Content []VolumeContent `yaml:"content"`
+}
+
+type VolumeContent struct {
+ Source string `yaml:"source"`
+ Target string `yaml:"target"`
+
+ Image string `yaml:"image"`
+ Offset string `yaml:"offset"`
+ OffsetWrite string `yaml:"offset-write"`
+ Size string `yaml:"size"`
+
+ Unpack bool `yaml:"unpack"`
+}
+
+func ReadGadgetInfo(info *Info) (*GadgetInfo, error) {
+ const errorFormat = "cannot read gadget snap details: %s"
+
+ gadgetYamlFn := filepath.Join(info.MountDir(), "meta", "gadget.yaml")
+ gmeta, err := ioutil.ReadFile(gadgetYamlFn)
+ if err != nil {
+ return nil, fmt.Errorf(errorFormat, err)
+ }
+
+ var gi GadgetInfo
+ if err := yaml.Unmarshal(gmeta, &gi); err != nil {
+ return nil, fmt.Errorf(errorFormat, err)
+ }
+
+ // basic validation
+ foundBootloader := false
+ for _, v := range gi.Volumes {
+ if foundBootloader {
+ return nil, fmt.Errorf(errorFormat, "bootloader already declared")
+ }
+ switch v.Bootloader {
+ case "":
+ return nil, fmt.Errorf(errorFormat, "bootloader cannot be empty")
+ case "grub", "u-boot":
+ foundBootloader = true
+ default:
+ return nil, fmt.Errorf(errorFormat, "bootloader must be either grub or u-boot")
+ }
+ }
+ if !foundBootloader {
+ return nil, fmt.Errorf(errorFormat, "bootloader not declared in any volume")
+ }
+
+ return &gi, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type gadgetYamlTestSuite struct {
+}
+
+var _ = Suite(&gadgetYamlTestSuite{})
+
+var mockGadgetSnapYaml = `
+name: canonical-pc
+type: gadget
+`
+
+var mockGadgetYaml = []byte(`
+defaults:
+ core:
+ something: true
+
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ id: id,guid
+ structure:
+ - label: system-boot
+ offset: 12345
+ offset-write: 777
+ size: 88888
+ type: id,guid
+ id: id,guid
+ filesystem: vfat
+ content:
+ - source: subdir/
+ target: /
+ unpack: false
+ - image: foo.img
+ offset: 4321
+ offset-write: 8888
+ size: 88888
+ unpack: false
+`)
+
+var mockGadgetSnapContents = "SNAP"
+
+func (s *gadgetYamlTestSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *gadgetYamlTestSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissing(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ _, err := snap.ReadGadgetInfo(info)
+ c.Assert(err, ErrorMatches, ".*meta/gadget.yaml: no such file or directory")
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlValid(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYaml, 0644)
+ c.Assert(err, IsNil)
+
+ ginfo, err := snap.ReadGadgetInfo(info)
+ c.Assert(err, IsNil)
+ c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{
+ Defaults: map[string]map[string]interface{}{
+ "core": {"something": true},
+ },
+ Volumes: map[string]snap.GadgetVolume{
+ "volumename": {
+ Schema: "mbr",
+ Bootloader: "u-boot",
+ ID: "id,guid",
+ Structure: []snap.VolumeStructure{
+ {
+ Label: "system-boot",
+ Offset: "12345",
+ OffsetWrite: "777",
+ Size: "88888",
+ Type: "id,guid",
+ ID: "id,guid",
+ Filesystem: "vfat",
+ Content: []snap.VolumeContent{
+ {
+ Source: "subdir/",
+ Target: "/",
+ Unpack: false,
+ },
+ {
+ Image: "foo.img",
+ Offset: "4321",
+ OffsetWrite: "8888",
+ Size: "88888",
+ Unpack: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlEmptydBootloader(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ mockGadgetYamlBroken := []byte(`
+volumes:
+ name:
+ bootloader:
+`)
+
+ err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadGadgetInfo(info)
+ c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader cannot be empty")
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidBootloader(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ mockGadgetYamlBroken := []byte(`
+volumes:
+ name:
+ bootloader: silo
+`)
+
+ err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadGadgetInfo(info)
+ c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader must be either grub or u-boot")
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissingBootloader(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+
+ err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadGadgetInfo(info)
+ c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader not declared in any volume")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "regexp"
+)
+
+var supportedHooks = []*HookType{
+ newHookType(regexp.MustCompile("^prepare-device$")),
+ newHookType(regexp.MustCompile("^configure$")),
+ newHookType(regexp.MustCompile("^prepare-(?:plug|slot)-[-a-z0-9]+$")),
+ newHookType(regexp.MustCompile("^connect-(?:plug|slot)-[-a-z0-9]+$")),
+}
+
+// HookType represents a pattern of supported hook names.
+type HookType struct {
+ pattern *regexp.Regexp
+}
+
+// newHookType returns a new HookType with the given pattern.
+func newHookType(pattern *regexp.Regexp) *HookType {
+ return &HookType{
+ pattern: pattern,
+ }
+}
+
+// Match returns true if the given hook name matches this hook type.
+func (hookType HookType) Match(hookName string) bool {
+ return hookType.pattern.MatchString(hookName)
+}
+
+// IsHookSupported returns true if the given hook name matches one of the
+// supported hooks.
+func IsHookSupported(hookName string) bool {
+ for _, hookType := range supportedHooks {
+ if hookType.Match(hookName) {
+ return true
+ }
+ }
+
+ return false
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/release"
+)
+
+var implicitSlots = []string{
+ "alsa",
+ "bluetooth-control",
+ "camera",
+ "dcdbas-control",
+ "docker-support",
+ "firewall-control",
+ "hardware-observe",
+ "home",
+ "io-ports-control",
+ "kernel-module-control",
+ "locale-control",
+ "log-observe",
+ "lxd-support",
+ "mount-observe",
+ "network",
+ "network-bind",
+ "network-control",
+ "network-observe",
+ "network-setup-observe",
+ "opengl",
+ "openvswitch-support",
+ "physical-memory-control",
+ "physical-memory-observe",
+ "ppp",
+ "process-control",
+ "raw-usb",
+ "removable-media",
+ "shutdown",
+ "snapd-control",
+ "system-observe",
+ "system-trace",
+ "time-control",
+ "timeserver-control",
+ "timezone-control",
+ "tpm",
+}
+
+var implicitClassicSlots = []string{
+ "avahi-observe",
+ "browser-support",
+ "cups-control",
+ "gsettings",
+ "libvirt",
+ "modem-manager",
+ "network-manager",
+ "ofono",
+ "openvswitch",
+ "optical-drive",
+ "pulseaudio",
+ "screen-inhibit-control",
+ "unity7",
+ "upower-observe",
+ "x11",
+}
+
+// AddImplicitSlots adds implicitly defined slots to a given snap.
+//
+// Only the OS snap has implicit slots.
+//
+// It is assumed that slots have names matching the interface name. Existing
+// slots are not changed, only missing slots are added.
+func AddImplicitSlots(snapInfo *Info) {
+ if snapInfo.Type != TypeOS {
+ return
+ }
+ for _, ifaceName := range implicitSlots {
+ if _, ok := snapInfo.Slots[ifaceName]; !ok {
+ snapInfo.Slots[ifaceName] = makeImplicitSlot(snapInfo, ifaceName)
+ }
+ }
+ // fuse-support is disabled on trusty due to usage of fuse requiring access to mount.
+ // we do not want to widen the apparmor profile defined in fuse-support to support trusty
+ // right now.
+ if !(release.ReleaseInfo.ID == "ubuntu" && release.ReleaseInfo.VersionID == "14.04") {
+ snapInfo.Slots["fuse-support"] = makeImplicitSlot(snapInfo, "fuse-support")
+ }
+ if !release.OnClassic {
+ return
+ }
+ for _, ifaceName := range implicitClassicSlots {
+ if _, ok := snapInfo.Slots[ifaceName]; !ok {
+ snapInfo.Slots[ifaceName] = makeImplicitSlot(snapInfo, ifaceName)
+ }
+ }
+}
+
+func makeImplicitSlot(snapInfo *Info, ifaceName string) *SlotInfo {
+ return &SlotInfo{
+ Name: ifaceName,
+ Snap: snapInfo,
+ Interface: ifaceName,
+ }
+}
+
+// addImplicitHooks adds hooks from the installed snap's hookdir to the snap info.
+//
+// Existing hooks (i.e. ones defined in the YAML) are not changed; only missing
+// hooks are added.
+func addImplicitHooks(snapInfo *Info) error {
+ // First of all, check to ensure the hooks directory exists. If it doesn't,
+ // it's not an error-- there's just nothing to do.
+ hooksDir := snapInfo.HooksDir()
+ if !osutil.IsDirectory(hooksDir) {
+ return nil
+ }
+
+ fileInfos, err := ioutil.ReadDir(hooksDir)
+ if err != nil {
+ return fmt.Errorf("unable to read hooks directory: %s", err)
+ }
+
+ for _, fileInfo := range fileInfos {
+ addHookIfValid(snapInfo, fileInfo.Name())
+ }
+
+ return nil
+}
+
+// addImplicitHooksFromContainer adds hooks from the snap file's hookdir to the snap info.
+//
+// Existing hooks (i.e. ones defined in the YAML) are not changed; only missing
+// hooks are added.
+func addImplicitHooksFromContainer(snapInfo *Info, snapf Container) error {
+ // Read the hooks directory. If this fails we assume the hooks directory
+ // doesn't exist, which means there are no implicit hooks to load (not an
+ // error).
+ fileNames, err := snapf.ListDir("meta/hooks")
+ if err != nil {
+ return nil
+ }
+
+ for _, fileName := range fileNames {
+ addHookIfValid(snapInfo, fileName)
+ }
+
+ return nil
+}
+
+func addHookIfValid(snapInfo *Info, hookName string) {
+ // Verify that the hook name is actually supported. If not, ignore it.
+ if !IsHookSupported(hookName) {
+ return
+ }
+
+ // Don't overwrite a hook that has already been loaded from the YAML
+ if _, ok := snapInfo.Hooks[hookName]; !ok {
+ snapInfo.Hooks[hookName] = &HookInfo{Snap: snapInfo, Name: hookName}
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+
+ . "gopkg.in/check.v1"
+)
+
+type SpecialSuite struct{}
+
+var _ = Suite(&SpecialSuite{})
+
+func (s *InfoSnapYamlTestSuite) TestAddImplicitSlotsOutsideClassic(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ osYaml := []byte("name: ubuntu-core\ntype: os\n")
+ info, err := snap.InfoFromSnapYaml(osYaml)
+ c.Assert(err, IsNil)
+ snap.AddImplicitSlots(info)
+ c.Assert(info.Slots["network"].Interface, Equals, "network")
+ c.Assert(info.Slots["network"].Name, Equals, "network")
+ c.Assert(info.Slots["network"].Snap, Equals, info)
+ // Ensure that we have *some* implicit slots
+ c.Assert(len(info.Slots) > 10, Equals, true)
+}
+
+func (s *InfoSnapYamlTestSuite) TestAddImplicitSlotsOnClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ osYaml := []byte("name: ubuntu-core\ntype: os\n")
+ info, err := snap.InfoFromSnapYaml(osYaml)
+ c.Assert(err, IsNil)
+ snap.AddImplicitSlots(info)
+ c.Assert(info.Slots["unity7"].Interface, Equals, "unity7")
+ c.Assert(info.Slots["unity7"].Name, Equals, "unity7")
+ c.Assert(info.Slots["unity7"].Snap, Equals, info)
+ // Ensure that we have *some* implicit slots
+ c.Assert(len(info.Slots) > 10, Equals, true)
+}
+
+func (s *InfoSnapYamlTestSuite) TestImplicitSlotsAreRealInterfaces(c *C) {
+ known := make(map[string]bool)
+ for _, iface := range builtin.Interfaces() {
+ known[iface.Name()] = true
+ }
+ for _, ifaceName := range snap.ImplicitSlotsForTests {
+ c.Check(known[ifaceName], Equals, true)
+ }
+ for _, ifaceName := range snap.ImplicitClassicSlotsForTests {
+ c.Check(known[ifaceName], Equals, true)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/timeout"
+)
+
+// PlaceInfo offers all the information about where a snap and its data are located and exposed in the filesystem.
+type PlaceInfo interface {
+ // Name returns the name of the snap.
+ Name() string
+
+ // MountDir returns the base directory of the snap.
+ MountDir() string
+
+ // MountFile returns the path where the snap file that is mounted is installed.
+ MountFile() string
+
+ // HooksDir returns the directory containing the snap's hooks.
+ HooksDir() string
+
+ // DataDir returns the data directory of the snap.
+ DataDir() string
+
+ // CommonDataDir returns the data directory common across revisions of the snap.
+ CommonDataDir() string
+
+ // DataHomeDir returns the per user data directory of the snap.
+ DataHomeDir() string
+
+ // CommonDataHomeDir returns the per user data directory common across revisions of the snap.
+ CommonDataHomeDir() string
+
+ // XdgRuntimeDirs returns the XDG_RUNTIME_DIR directories for all users of the snap.
+ XdgRuntimeDirs() string
+}
+
+// MinimalPlaceInfo returns a PlaceInfo with just the location information for a snap of the given name and revision.
+func MinimalPlaceInfo(name string, revision Revision) PlaceInfo {
+ return &Info{SideInfo: SideInfo{RealName: name, Revision: revision}}
+}
+
+// MountDir returns the base directory where it gets mounted of the snap with the given name and revision.
+func MountDir(name string, revision Revision) string {
+ return filepath.Join(dirs.SnapMountDir, name, revision.String())
+}
+
+// MountFile returns the path where the snap file that is mounted is installed.
+func MountFile(name string, revision Revision) string {
+ return filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%s.snap", name, revision))
+}
+
+// ScopedSecurityTag returns the snap-specific, scope specific, security tag.
+func ScopedSecurityTag(snapName, scopeName, suffix string) string {
+ return fmt.Sprintf("snap.%s.%s.%s", snapName, scopeName, suffix)
+}
+
+// SecurityTag returns the snap-specific security tag.
+func SecurityTag(snapName string) string {
+ return fmt.Sprintf("snap.%s", snapName)
+}
+
+// AppSecurityTag returns the application-specific security tag.
+func AppSecurityTag(snapName, appName string) string {
+ return fmt.Sprintf("%s.%s", SecurityTag(snapName), appName)
+}
+
+// HookSecurityTag returns the hook-specific security tag.
+func HookSecurityTag(snapName, hookName string) string {
+ return ScopedSecurityTag(snapName, "hook", hookName)
+}
+
+// NoneSecurityTag returns the security tag for interfaces that
+// are not associated to an app or hook in the snap.
+func NoneSecurityTag(snapName, uniqueName string) string {
+ return ScopedSecurityTag(snapName, "none", uniqueName)
+}
+
+// SideInfo holds snap metadata that is crucial for the tracking of
+// snaps and for the working of the system offline and which is not
+// included in snap.yaml or for which the store is the canonical
+// source overriding snap.yaml content.
+//
+// It can be marshalled and will be stored in the system state for
+// each currently installed snap revision so it needs to be evolved
+// carefully.
+//
+// Information that can be taken directly from snap.yaml or that comes
+// from the store but is not required for working offline should not
+// end up in SideInfo.
+type SideInfo struct {
+ RealName string `yaml:"name,omitempty" json:"name,omitempty"`
+ SnapID string `yaml:"snap-id" json:"snap-id"`
+ Revision Revision `yaml:"revision" json:"revision"`
+ Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
+ EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
+ Private bool `yaml:"private,omitempty" json:"private,omitempty"`
+}
+
+// Info provides information about snaps.
+type Info struct {
+ SuggestedName string
+ Version string
+ Type Type
+ Architectures []string
+ Assumes []string
+
+ OriginalSummary string
+ OriginalDescription string
+
+ Environment map[string]string
+
+ LicenseAgreement string
+ LicenseVersion string
+ Epoch string
+ Confinement ConfinementType
+ Apps map[string]*AppInfo
+ Aliases map[string]*AppInfo
+ Hooks map[string]*HookInfo
+ Plugs map[string]*PlugInfo
+ Slots map[string]*SlotInfo
+
+ // The information in all the remaining fields is not sourced from the snap blob itself.
+ SideInfo
+
+ // Broken marks if set whether the snap is broken and the reason.
+ Broken string
+
+ // The information in these fields is ephemeral, available only from the store.
+ DownloadInfo
+
+ IconURL string
+ Prices map[string]float64
+ MustBuy bool
+
+ PublisherID string
+ Publisher string
+
+ Screenshots []ScreenshotInfo
+ Channels map[string]*ChannelSnapInfo
+}
+
+// ChannelSnapInfo is the minimum information that can be used to clearly
+// distinguish different revisions of the same snap.
+type ChannelSnapInfo struct {
+ Revision Revision `json:"revision"`
+ Confinement ConfinementType `json:"confinement"`
+ Version string `json:"version"`
+ Channel string `json:"channel"`
+ Epoch string `json:"epoch"`
+ Size int64 `json:"size"`
+}
+
+// Name returns the blessed name for the snap.
+func (s *Info) Name() string {
+ if s.RealName != "" {
+ return s.RealName
+ }
+ return s.SuggestedName
+}
+
+// Summary returns the blessed summary for the snap.
+func (s *Info) Summary() string {
+ if s.EditedSummary != "" {
+ return s.EditedSummary
+ }
+ return s.OriginalSummary
+}
+
+// Description returns the blessed description for the snap.
+func (s *Info) Description() string {
+ if s.EditedDescription != "" {
+ return s.EditedDescription
+ }
+ return s.OriginalDescription
+}
+
+// MountDir returns the base directory of the snap where it gets mounted.
+func (s *Info) MountDir() string {
+ return MountDir(s.Name(), s.Revision)
+}
+
+// MountFile returns the path where the snap file that is mounted is installed.
+func (s *Info) MountFile() string {
+ return MountFile(s.Name(), s.Revision)
+}
+
+// HooksDir returns the directory containing the snap's hooks.
+func (s *Info) HooksDir() string {
+ return filepath.Join(s.MountDir(), "meta", "hooks")
+}
+
+// DataDir returns the data directory of the snap.
+func (s *Info) DataDir() string {
+ return filepath.Join(dirs.SnapDataDir, s.Name(), s.Revision.String())
+}
+
+// UserDataDir returns the user-specific data directory of the snap.
+func (s *Info) UserDataDir(home string) string {
+ return filepath.Join(home, "snap", s.Name(), s.Revision.String())
+}
+
+// UserCommonDataDir returns the user-specific data directory common across revision of the snap.
+func (s *Info) UserCommonDataDir(home string) string {
+ return filepath.Join(home, "snap", s.Name(), "common")
+}
+
+// CommonDataDir returns the data directory common across revisions of the snap.
+func (s *Info) CommonDataDir() string {
+ return filepath.Join(dirs.SnapDataDir, s.Name(), "common")
+}
+
+// DataHomeDir returns the per user data directory of the snap.
+func (s *Info) DataHomeDir() string {
+ return filepath.Join(dirs.SnapDataHomeGlob, s.Name(), s.Revision.String())
+}
+
+// CommonDataHomeDir returns the per user data directory common across revisions of the snap.
+func (s *Info) CommonDataHomeDir() string {
+ return filepath.Join(dirs.SnapDataHomeGlob, s.Name(), "common")
+}
+
+// UserXdgRuntimeDir returns the XDG_RUNTIME_DIR directory of the snap for a particular user.
+func (s *Info) UserXdgRuntimeDir(euid int) string {
+ return filepath.Join("/run/user", fmt.Sprintf("%d/snap.%s", euid, s.Name()))
+}
+
+// XdgRuntimeDirs returns the XDG_RUNTIME_DIR directories for all users of the snap.
+func (s *Info) XdgRuntimeDirs() string {
+ return filepath.Join(dirs.XdgRuntimeDirGlob, fmt.Sprintf("snap.%s", s.Name()))
+}
+
+// NeedsDevMode returns whether the snap needs devmode.
+func (s *Info) NeedsDevMode() bool {
+ return s.Confinement == DevModeConfinement
+}
+
+// NeedsClassic returns whether the snap needs classic confinement consent.
+func (s *Info) NeedsClassic() bool {
+ return s.Confinement == ClassicConfinement
+}
+
+// DownloadInfo contains the information to download a snap.
+// It can be marshalled.
+type DownloadInfo struct {
+ AnonDownloadURL string `json:"anon-download-url,omitempty"`
+ DownloadURL string `json:"download-url,omitempty"`
+
+ Size int64 `json:"size,omitempty"`
+ Sha3_384 string `json:"sha3-384,omitempty"`
+
+ // The server can include information about available deltas for a given
+ // snap at a specific revision during refresh. Currently during refresh the
+ // server will provide single matching deltas only, from the clients
+ // revision to the target revision when available, per requested format.
+ Deltas []DeltaInfo `json:"deltas,omitempty"`
+}
+
+// DeltaInfo contains the information to download a delta
+// from one revision to another.
+type DeltaInfo struct {
+ FromRevision int `json:"from-revision,omitempty"`
+ ToRevision int `json:"to-revision,omitempty"`
+ Format string `json:"format,omitempty"`
+ AnonDownloadURL string `json:"anon-download-url,omitempty"`
+ DownloadURL string `json:"download-url,omitempty"`
+ Size int64 `json:"size,omitempty"`
+ Sha3_384 string `json:"sha3-384,omitempty"`
+}
+
+// sanity check that Info is a PlaceInfo
+var _ PlaceInfo = (*Info)(nil)
+
+// PlugInfo provides information about a plug.
+type PlugInfo struct {
+ Snap *Info
+
+ Name string
+ Interface string
+ Attrs map[string]interface{}
+ Label string
+ Apps map[string]*AppInfo
+ Hooks map[string]*HookInfo
+}
+
+// SlotInfo provides information about a slot.
+type SlotInfo struct {
+ Snap *Info
+
+ Name string
+ Interface string
+ Attrs map[string]interface{}
+ Label string
+ Apps map[string]*AppInfo
+}
+
+// AppInfo provides information about a app.
+type AppInfo struct {
+ Snap *Info
+
+ Name string
+ Aliases []string
+ Command string
+
+ Daemon string
+ StopTimeout timeout.Timeout
+ StopCommand string
+ PostStopCommand string
+ RestartCond systemd.RestartCondition
+
+ // TODO: this should go away once we have more plumbing and can change
+ // things vs refactor
+ // https://github.com/snapcore/snapd/pull/794#discussion_r58688496
+ BusName string
+
+ Plugs map[string]*PlugInfo
+ Slots map[string]*SlotInfo
+
+ Environment map[string]string
+}
+
+// ScreenshotInfo provides information about a screenshot.
+type ScreenshotInfo struct {
+ URL string
+ Width int64
+ Height int64
+}
+
+// HookInfo provides information about a hook.
+type HookInfo struct {
+ Snap *Info
+
+ Name string
+ Plugs map[string]*PlugInfo
+}
+
+// SecurityTag returns application-specific security tag.
+//
+// Security tags are used by various security subsystems as "profile names" and
+// sometimes also as a part of the file name.
+func (app *AppInfo) SecurityTag() string {
+ return AppSecurityTag(app.Snap.Name(), app.Name)
+}
+
+// WrapperPath returns the path to wrapper invoking the app binary.
+func (app *AppInfo) WrapperPath() string {
+ var binName string
+ if app.Name == app.Snap.Name() {
+ binName = filepath.Base(app.Name)
+ } else {
+ binName = fmt.Sprintf("%s.%s", app.Snap.Name(), filepath.Base(app.Name))
+ }
+
+ return filepath.Join(dirs.SnapBinariesDir, binName)
+}
+
+func (app *AppInfo) launcherCommand(command string) string {
+ if command != "" {
+ command = " " + command
+ }
+ if app.Name == app.Snap.Name() {
+ return fmt.Sprintf("/usr/bin/snap run%s %s", command, app.Name)
+ }
+ return fmt.Sprintf("/usr/bin/snap run%s %s.%s", command, app.Snap.Name(), filepath.Base(app.Name))
+}
+
+// LauncherCommand returns the launcher command line to use when invoking the app binary.
+func (app *AppInfo) LauncherCommand() string {
+ return app.launcherCommand("")
+}
+
+// LauncherStopCommand returns the launcher command line to use when invoking the app stop command binary.
+func (app *AppInfo) LauncherStopCommand() string {
+ return app.launcherCommand("--command=stop")
+}
+
+// LauncherPostStopCommand returns the launcher command line to use when invoking the app post-stop command binary.
+func (app *AppInfo) LauncherPostStopCommand() string {
+ return app.launcherCommand("--command=post-stop")
+}
+
+// ServiceFile returns the systemd service file path for the daemon app.
+func (app *AppInfo) ServiceFile() string {
+ return filepath.Join(dirs.SnapServicesDir, app.SecurityTag()+".service")
+}
+
+// ServiceSocketFile returns the systemd socket file path for the daemon app.
+func (app *AppInfo) ServiceSocketFile() string {
+ return filepath.Join(dirs.SnapServicesDir, app.SecurityTag()+".socket")
+}
+
+func copyEnv(in map[string]string) map[string]string {
+ out := make(map[string]string)
+ for k, v := range in {
+ out[k] = v
+ }
+
+ return out
+}
+
+// Env returns the app specific environment overrides
+func (app *AppInfo) Env() []string {
+ env := []string{}
+ appEnv := copyEnv(app.Snap.Environment)
+ for k, v := range app.Environment {
+ appEnv[k] = v
+ }
+ for k, v := range appEnv {
+ env = append(env, fmt.Sprintf("%s=%s\n", k, v))
+ }
+ return env
+}
+
+// SecurityTag returns the hook-specific security tag.
+//
+// Security tags are used by various security subsystems as "profile names" and
+// sometimes also as a part of the file name.
+func (hook *HookInfo) SecurityTag() string {
+ return HookSecurityTag(hook.Snap.Name(), hook.Name)
+}
+
+// Env returns the hook-specific environment overrides
+func (hook *HookInfo) Env() []string {
+ env := []string{}
+ hookEnv := copyEnv(hook.Snap.Environment)
+ for k, v := range hookEnv {
+ env = append(env, fmt.Sprintf("%s=%s\n", k, v))
+ }
+ return env
+}
+
+func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo) (*Info, error) {
+ info, err := InfoFromSnapYaml(meta)
+ if err != nil {
+ return nil, err
+ }
+
+ if si != nil {
+ info.SideInfo = *si
+ }
+
+ return info, nil
+}
+
+type NotFoundError struct {
+ Snap string
+ Revision Revision
+}
+
+func (e NotFoundError) Error() string {
+ return fmt.Sprintf("cannot find installed snap %q at revision %s", e.Snap, e.Revision)
+}
+
+// ReadInfo reads the snap information for the installed snap with the given name and given side-info.
+func ReadInfo(name string, si *SideInfo) (*Info, error) {
+ snapYamlFn := filepath.Join(MountDir(name, si.Revision), "meta", "snap.yaml")
+ meta, err := ioutil.ReadFile(snapYamlFn)
+ if os.IsNotExist(err) {
+ return nil, &NotFoundError{Snap: name, Revision: si.Revision}
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ info, err := infoFromSnapYamlWithSideInfo(meta, si)
+ if err != nil {
+ return nil, err
+ }
+
+ st, err := os.Stat(MountFile(name, si.Revision))
+ if err != nil {
+ return nil, err
+ }
+ info.Size = st.Size()
+
+ err = addImplicitHooks(info)
+ if err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
+
+// ReadInfoFromSnapFile reads the snap information from the given File
+// and completes it with the given side-info if this is not nil.
+func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) {
+ meta, err := snapf.ReadFile("meta/snap.yaml")
+ if err != nil {
+ return nil, err
+ }
+
+ info, err := infoFromSnapYamlWithSideInfo(meta, si)
+ if err != nil {
+ return nil, err
+ }
+
+ info.Size, err = snapf.Size()
+ if err != nil {
+ return nil, err
+ }
+
+ err = addImplicitHooksFromContainer(info, snapf)
+ if err != nil {
+ return nil, err
+ }
+
+ err = Validate(info)
+ if err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
+
+// SplitSnapApp will split a string of the form `snap.app` into
+// the `snap` and the `app` part. It also deals with the special
+// case of snapName == appName.
+func SplitSnapApp(snapApp string) (snap, app string) {
+ l := strings.SplitN(snapApp, ".", 2)
+ if len(l) < 2 {
+ return l[0], l[0]
+ }
+ return l[0], l[1]
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/timeout"
+)
+
+type snapYaml struct {
+ Name string `yaml:"name"`
+ Version string `yaml:"version"`
+ Type Type `yaml:"type"`
+ Architectures []string `yaml:"architectures,omitempty"`
+ Assumes []string `yaml:"assumes"`
+ Description string `yaml:"description"`
+ Summary string `yaml:"summary"`
+ LicenseAgreement string `yaml:"license-agreement,omitempty"`
+ LicenseVersion string `yaml:"license-version,omitempty"`
+ Epoch string `yaml:"epoch,omitempty"`
+ Confinement ConfinementType `yaml:"confinement,omitempty"`
+ Environment map[string]string `yaml:"environment,omitempty"`
+ Plugs map[string]interface{} `yaml:"plugs,omitempty"`
+ Slots map[string]interface{} `yaml:"slots,omitempty"`
+ Apps map[string]appYaml `yaml:"apps,omitempty"`
+ Hooks map[string]hookYaml `yaml:"hooks,omitempty"`
+}
+
+type appYaml struct {
+ Aliases []string `yaml:"aliases,omitempty"`
+
+ Command string `yaml:"command"`
+
+ Daemon string `yaml:"daemon"`
+
+ StopCommand string `yaml:"stop-command,omitempty"`
+ PostStopCommand string `yaml:"post-stop-command,omitempty"`
+ StopTimeout timeout.Timeout `yaml:"stop-timeout,omitempty"`
+
+ RestartCond systemd.RestartCondition `yaml:"restart-condition,omitempty"`
+ SlotNames []string `yaml:"slots,omitempty"`
+ PlugNames []string `yaml:"plugs,omitempty"`
+
+ BusName string `yaml:"bus-name,omitempty"`
+
+ Environment map[string]string `yaml:"environment,omitempty"`
+}
+
+type hookYaml struct {
+ PlugNames []string `yaml:"plugs,omitempty"`
+}
+
+// InfoFromSnapYaml creates a new info based on the given snap.yaml data
+func InfoFromSnapYaml(yamlData []byte) (*Info, error) {
+ var y snapYaml
+ err := yaml.Unmarshal(yamlData, &y)
+ if err != nil {
+ return nil, fmt.Errorf("info failed to parse: %s", err)
+ }
+
+ snap := infoSkeletonFromSnapYaml(y)
+ setEnvironmentFromSnapYaml(y, snap)
+
+ // Collect top-level definitions of plugs and slots
+ if err := setPlugsFromSnapYaml(y, snap); err != nil {
+ return nil, err
+ }
+ if err := setSlotsFromSnapYaml(y, snap); err != nil {
+ return nil, err
+ }
+
+ // At this point snap.Plugs and snap.Slots only contain globally-declared
+ // plugs and slots. We're about to change that, but we need to remember the
+ // global ones for later, so save their names.
+ globalPlugNames := make([]string, 0, len(snap.Plugs))
+ for plugName := range snap.Plugs {
+ globalPlugNames = append(globalPlugNames, plugName)
+ }
+
+ globalSlotNames := make([]string, 0, len(snap.Slots))
+ for slotName := range snap.Slots {
+ globalSlotNames = append(globalSlotNames, slotName)
+ }
+
+ // Collect all apps, their aliases and hooks
+ if err := setAppsFromSnapYaml(y, snap); err != nil {
+ return nil, err
+ }
+ setHooksFromSnapYaml(y, snap)
+
+ // Bind unbound plugs to all apps and hooks
+ bindUnboundPlugs(globalPlugNames, snap)
+
+ // Bind unbound slots to all apps
+ bindUnboundSlots(globalSlotNames, snap)
+
+ // FIXME: validation of the fields
+ return snap, nil
+}
+
+// infoSkeletonFromSnapYaml initializes an Info without apps, hook, plugs, or
+// slots
+func infoSkeletonFromSnapYaml(y snapYaml) *Info {
+ // Prepare defaults
+ architectures := []string{"all"}
+ if len(y.Architectures) != 0 {
+ architectures = y.Architectures
+ }
+ typ := TypeApp
+ if y.Type != "" {
+ typ = y.Type
+ }
+ epoch := "0"
+ if y.Epoch != "" {
+ epoch = y.Epoch
+ }
+ confinement := StrictConfinement
+ if y.Confinement != "" {
+ confinement = y.Confinement
+ }
+
+ // Construct snap skeleton without apps, hooks, plugs, or slots
+ snap := &Info{
+ SuggestedName: y.Name,
+ Version: y.Version,
+ Type: typ,
+ Architectures: architectures,
+ Assumes: y.Assumes,
+ OriginalDescription: y.Description,
+ OriginalSummary: y.Summary,
+ LicenseAgreement: y.LicenseAgreement,
+ LicenseVersion: y.LicenseVersion,
+ Epoch: epoch,
+ Confinement: confinement,
+ Apps: make(map[string]*AppInfo),
+ Aliases: make(map[string]*AppInfo),
+ Hooks: make(map[string]*HookInfo),
+ Plugs: make(map[string]*PlugInfo),
+ Slots: make(map[string]*SlotInfo),
+ Environment: y.Environment,
+ }
+
+ sort.Strings(snap.Assumes)
+
+ return snap
+}
+
+func setEnvironmentFromSnapYaml(y snapYaml, snap *Info) {
+ for k, v := range y.Environment {
+ snap.Environment[k] = v
+ }
+}
+
+func setPlugsFromSnapYaml(y snapYaml, snap *Info) error {
+ for name, data := range y.Plugs {
+ iface, label, attrs, err := convertToSlotOrPlugData("plug", name, data)
+ if err != nil {
+ return err
+ }
+ snap.Plugs[name] = &PlugInfo{
+ Snap: snap,
+ Name: name,
+ Interface: iface,
+ Attrs: attrs,
+ Label: label,
+ }
+ if len(y.Apps) > 0 {
+ snap.Plugs[name].Apps = make(map[string]*AppInfo)
+ }
+ if len(y.Hooks) > 0 {
+ snap.Plugs[name].Hooks = make(map[string]*HookInfo)
+ }
+ }
+
+ return nil
+}
+
+func setSlotsFromSnapYaml(y snapYaml, snap *Info) error {
+ for name, data := range y.Slots {
+ iface, label, attrs, err := convertToSlotOrPlugData("slot", name, data)
+ if err != nil {
+ return err
+ }
+ snap.Slots[name] = &SlotInfo{
+ Snap: snap,
+ Name: name,
+ Interface: iface,
+ Attrs: attrs,
+ Label: label,
+ }
+ if len(y.Apps) > 0 {
+ snap.Slots[name].Apps = make(map[string]*AppInfo)
+ }
+ }
+
+ return nil
+}
+
+func setAppsFromSnapYaml(y snapYaml, snap *Info) error {
+ for appName, yApp := range y.Apps {
+ // Collect all apps
+ app := &AppInfo{
+ Snap: snap,
+ Name: appName,
+ Aliases: yApp.Aliases,
+ Command: yApp.Command,
+ Daemon: yApp.Daemon,
+ StopTimeout: yApp.StopTimeout,
+ StopCommand: yApp.StopCommand,
+ PostStopCommand: yApp.PostStopCommand,
+ RestartCond: yApp.RestartCond,
+ BusName: yApp.BusName,
+ Environment: yApp.Environment,
+ }
+ if len(y.Plugs) > 0 || len(yApp.PlugNames) > 0 {
+ app.Plugs = make(map[string]*PlugInfo)
+ }
+ if len(y.Slots) > 0 || len(yApp.SlotNames) > 0 {
+ app.Slots = make(map[string]*SlotInfo)
+ }
+ snap.Apps[appName] = app
+ for _, alias := range app.Aliases {
+ if snap.Aliases[alias] != nil {
+ return fmt.Errorf("cannot set %q as alias for both %q and %q", alias, snap.Aliases[alias].Name, appName)
+ }
+ snap.Aliases[alias] = app
+ }
+ // Bind all plugs/slots listed in this app
+ for _, plugName := range yApp.PlugNames {
+ plug, ok := snap.Plugs[plugName]
+ if !ok {
+ // Create implicit plug definitions if required
+ plug = &PlugInfo{
+ Snap: snap,
+ Name: plugName,
+ Interface: plugName,
+ Apps: make(map[string]*AppInfo),
+ }
+ snap.Plugs[plugName] = plug
+ }
+ app.Plugs[plugName] = plug
+ plug.Apps[appName] = app
+ }
+ for _, slotName := range yApp.SlotNames {
+ slot, ok := snap.Slots[slotName]
+ if !ok {
+ slot = &SlotInfo{
+ Snap: snap,
+ Name: slotName,
+ Interface: slotName,
+ Apps: make(map[string]*AppInfo),
+ }
+ snap.Slots[slotName] = slot
+ }
+ app.Slots[slotName] = slot
+ slot.Apps[appName] = app
+ }
+ }
+ return nil
+}
+
+func setHooksFromSnapYaml(y snapYaml, snap *Info) {
+ for hookName, yHook := range y.Hooks {
+ if !IsHookSupported(hookName) {
+ continue
+ }
+
+ // Collect all hooks
+ hook := &HookInfo{
+ Snap: snap,
+ Name: hookName,
+ }
+ if len(y.Plugs) > 0 || len(yHook.PlugNames) > 0 {
+ hook.Plugs = make(map[string]*PlugInfo)
+ }
+ snap.Hooks[hookName] = hook
+ // Bind all plugs/slots listed in this hook
+ for _, plugName := range yHook.PlugNames {
+ plug, ok := snap.Plugs[plugName]
+ if !ok {
+ // Create implicit plug definitions if required
+ plug = &PlugInfo{
+ Snap: snap,
+ Name: plugName,
+ Interface: plugName,
+ Hooks: make(map[string]*HookInfo),
+ }
+ snap.Plugs[plugName] = plug
+ } else if plug.Hooks == nil {
+ plug.Hooks = make(map[string]*HookInfo)
+ }
+ hook.Plugs[plugName] = plug
+ plug.Hooks[hookName] = hook
+ }
+ }
+}
+
+func bindUnboundPlugs(plugNames []string, snap *Info) error {
+ for _, plugName := range plugNames {
+ plug, ok := snap.Plugs[plugName]
+ if !ok {
+ return fmt.Errorf("no plug named %q", plugName)
+ }
+
+ // A plug is considered unbound if it isn't being used by any apps
+ // or hooks. In which case we bind them to all apps and hooks.
+ if len(plug.Apps) == 0 && len(plug.Hooks) == 0 {
+ for appName, app := range snap.Apps {
+ app.Plugs[plugName] = plug
+ plug.Apps[appName] = app
+ }
+
+ for hookName, hook := range snap.Hooks {
+ hook.Plugs[plugName] = plug
+ plug.Hooks[hookName] = hook
+ }
+ }
+ }
+
+ return nil
+}
+
+func bindUnboundSlots(slotNames []string, snap *Info) error {
+ for _, slotName := range slotNames {
+ slot, ok := snap.Slots[slotName]
+ if !ok {
+ return fmt.Errorf("no slot named %q", slotName)
+ }
+
+ if len(slot.Apps) == 0 {
+ for appName, app := range snap.Apps {
+ app.Slots[slotName] = slot
+ slot.Apps[appName] = app
+ }
+ }
+ }
+
+ return nil
+}
+
+func convertToSlotOrPlugData(plugOrSlot, name string, data interface{}) (iface, label string, attrs map[string]interface{}, err error) {
+ iface = name
+ switch data.(type) {
+ case string:
+ return data.(string), "", nil, nil
+ case nil:
+ return name, "", nil, nil
+ case map[interface{}]interface{}:
+ for keyData, valueData := range data.(map[interface{}]interface{}) {
+ key, ok := keyData.(string)
+ if !ok {
+ err := fmt.Errorf("%s %q has attribute that is not a string (found %T)",
+ plugOrSlot, name, keyData)
+ return "", "", nil, err
+ }
+ if strings.HasPrefix(key, "$") {
+ err := fmt.Errorf("%s %q uses reserved attribute %q", plugOrSlot, name, key)
+ return "", "", nil, err
+ }
+ switch key {
+ case "interface":
+ value, ok := valueData.(string)
+ if !ok {
+ err := fmt.Errorf("interface name on %s %q is not a string (found %T)",
+ plugOrSlot, name, valueData)
+ return "", "", nil, err
+ }
+ iface = value
+ case "label":
+ value, ok := valueData.(string)
+ if !ok {
+ err := fmt.Errorf("label of %s %q is not a string (found %T)",
+ plugOrSlot, name, valueData)
+ return "", "", nil, err
+ }
+ label = value
+ default:
+ if attrs == nil {
+ attrs = make(map[string]interface{})
+ }
+ value, err := validateAttr(valueData)
+ if err != nil {
+ return "", "", nil, fmt.Errorf("attribute %q of %s %q: %v", key, plugOrSlot, name, err)
+ }
+ attrs[key] = value
+ }
+ }
+ return iface, label, attrs, nil
+ default:
+ err := fmt.Errorf("%s %q has malformed definition (found %T)", plugOrSlot, name, data)
+ return "", "", nil, err
+ }
+}
+
+// validateAttr validates an attribute value and returns a normalized version of it (map[interface{}]interface{} is turned into map[string]interface{})
+func validateAttr(v interface{}) (interface{}, error) {
+ switch x := v.(type) {
+ case string:
+ return x, nil
+ case bool:
+ return x, nil
+ case int:
+ return int64(x), nil
+ case int64:
+ return x, nil
+ case []interface{}:
+ l := make([]interface{}, len(x))
+ for i, el := range x {
+ el, err := validateAttr(el)
+ if err != nil {
+ return nil, err
+ }
+ l[i] = el
+ }
+ return l, nil
+ case map[interface{}]interface{}:
+ m := make(map[string]interface{}, len(x))
+ for k, item := range x {
+ kStr, ok := k.(string)
+ if !ok {
+ return nil, fmt.Errorf("non-string key in attribute map: %v", k)
+ }
+ item, err := validateAttr(item)
+ if err != nil {
+ return nil, err
+ }
+ m[kStr] = item
+ }
+ return m, nil
+ default:
+ return nil, fmt.Errorf("invalid attribute scalar: %v", v)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "regexp"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/timeout"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type InfoSnapYamlTestSuite struct{}
+
+var _ = Suite(&InfoSnapYamlTestSuite{})
+
+var mockYaml = []byte(`name: foo
+version: 1.0
+type: app
+`)
+
+func (s *InfoSnapYamlTestSuite) TestSimple(c *C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, IsNil)
+ c.Assert(info.Name(), Equals, "foo")
+ c.Assert(info.Version, Equals, "1.0")
+ c.Assert(info.Type, Equals, snap.TypeApp)
+}
+
+func (s *InfoSnapYamlTestSuite) TestFail(c *C) {
+ _, err := snap.InfoFromSnapYaml([]byte("random-crap"))
+ c.Assert(err, ErrorMatches, "(?m)info failed to parse:.*")
+}
+
+type YamlSuite struct {
+ restore func()
+}
+
+var _ = Suite(&YamlSuite{})
+
+func (s *YamlSuite) SetUpTest(c *C) {
+ hookType := snap.NewHookType(regexp.MustCompile(".*"))
+ s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
+}
+
+func (s *YamlSuite) TearDownTest(c *C) {
+ s.restore()
+}
+
+func (s *YamlSuite) TestUnmarshalGarbage(c *C) {
+ _, err := snap.InfoFromSnapYaml([]byte(`"`))
+ c.Assert(err, ErrorMatches, ".*: yaml: found unexpected end of stream")
+}
+
+func (s *YamlSuite) TestUnmarshalEmpty(c *C) {
+ info, err := snap.InfoFromSnapYaml([]byte(``))
+ c.Assert(err, IsNil)
+ c.Assert(info.Plugs, HasLen, 0)
+ c.Assert(info.Slots, HasLen, 0)
+ c.Assert(info.Apps, HasLen, 0)
+}
+
+// Tests focusing on plugs
+
+func (s *YamlSuite) TestUnmarshalStandaloneImplicitPlug(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ network-client:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Assert(info.Plugs["network-client"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneAbbreviatedPlug(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net: network-client
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneMinimalisticPlug(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net:
+ interface: network-client
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneCompletePlug(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net:
+ interface: network-client
+ ipv6-aware: true
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ Attrs: map[string]interface{}{"ipv6-aware": true},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandalonePlugWithIntAndListAndMap(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ iface:
+ interface: complex
+ i: 3
+ l: [1,2,3]
+ m:
+ a: A
+ b: B
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Assert(info.Plugs["iface"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "iface",
+ Interface: "complex",
+ Attrs: map[string]interface{}{
+ "i": int64(3),
+ "l": []interface{}{int64(1), int64(2), int64(3)},
+ "m": map[string]interface{}{"a": "A", "b": "B"},
+ },
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalLastPlugDefinitionWins(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net:
+ interface: network-client
+ attr: 1
+ net:
+ interface: network-client
+ attr: 2
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ Attrs: map[string]interface{}{"attr": int64(2)},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalPlugsExplicitlyDefinedImplicitlyBoundToApps(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ network-client:
+apps:
+ app:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 1)
+
+ plug := info.Plugs["network-client"]
+ app := info.Apps["app"]
+ c.Assert(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{app.Name: app},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "app",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToOneApp(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ network-client:
+apps:
+ with-plug:
+ plugs: [network-client]
+ without-plug:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 2)
+
+ plug := info.Plugs["network-client"]
+ withPlugApp := info.Apps["with-plug"]
+ withoutPlugApp := info.Apps["without-plug"]
+ c.Assert(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{withPlugApp.Name: withPlugApp},
+ })
+ c.Assert(withPlugApp, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "with-plug",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+ c.Assert(withoutPlugApp, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "without-plug",
+ Plugs: map[string]*snap.PlugInfo{},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalPlugsExplicitlyDefinedExplicitlyBoundToApps(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net: network-client
+apps:
+ app:
+ plugs: ["net"]
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 1)
+ plug := info.Plugs["net"]
+ app := info.Apps["app"]
+ c.Assert(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{app.Name: app},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "app",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalPlugsImplicitlyDefinedExplicitlyBoundToApps(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+apps:
+ app:
+ plugs: ["network-client"]
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 1)
+ plug := info.Plugs["network-client"]
+ app := info.Apps["app"]
+ c.Assert(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{app.Name: app},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "app",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalPlugWithoutInterfaceName(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ network-client:
+ ipv6-aware: true
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Assert(info.Plugs["network-client"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Attrs: map[string]interface{}{"ipv6-aware": true},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalPlugWithLabel(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ bool-file:
+ label: Disk I/O indicator
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Assert(info.Plugs["bool-file"], DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "bool-file",
+ Interface: "bool-file",
+ Label: "Disk I/O indicator",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringInterfaceName(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net:
+ interface: 1.0
+ ipv6-aware: true
+`))
+ c.Assert(err, ErrorMatches, `interface name on plug "net" is not a string \(found float64\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringLabel(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ bool-file:
+ label: 1.0
+`))
+ c.Assert(err, ErrorMatches, `label of plug "bool-file" is not a string \(found float64\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringAttributes(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net:
+ 1: ok
+`))
+ c.Assert(err, ErrorMatches, `plug "net" has attribute that is not a string \(found int\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedPlugWithUnexpectedType(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net: 5
+`))
+ c.Assert(err, ErrorMatches, `plug "net" has malformed definition \(found int\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalReservedPlugAttribute(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ serial:
+ interface: serial-port
+ $baud-rate: [9600]
+`))
+ c.Assert(err, ErrorMatches, `plug "serial" uses reserved attribute "\$baud-rate"`)
+}
+
+func (s *YamlSuite) TestUnmarshalInvalidPlugAttribute(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ serial:
+ interface: serial-port
+ foo: null
+`))
+ c.Assert(err, ErrorMatches, `attribute "foo" of plug \"serial\": invalid attribute scalar:.*`)
+}
+
+func (s *YamlSuite) TestUnmarshalInvalidAttributeMapKey(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ serial:
+ interface: serial-port
+ bar:
+ baz:
+ - 1: A
+`))
+ c.Assert(err, ErrorMatches, `attribute "bar" of plug \"serial\": non-string key in attribute map: 1`)
+}
+
+// Tests focusing on slots
+
+func (s *YamlSuite) TestUnmarshalStandaloneImplicitSlot(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ network-client:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Assert(info.Slots["network-client"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneAbbreviatedSlot(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net: network-client
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneMinimalisticSlot(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net:
+ interface: network-client
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneCompleteSlot(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net:
+ interface: network-client
+ ipv6-aware: true
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ Attrs: map[string]interface{}{"ipv6-aware": true},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalStandaloneSlotWithIntAndListAndMap(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ iface:
+ interface: complex
+ i: 3
+ l: [1,2]
+ m:
+ a: "A"
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Assert(info.Slots["iface"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "iface",
+ Interface: "complex",
+ Attrs: map[string]interface{}{
+ "i": int64(3),
+ "l": []interface{}{int64(1), int64(2)},
+ "m": map[string]interface{}{"a": "A"},
+ },
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalLastSlotDefinitionWins(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net:
+ interface: network-client
+ attr: 1
+ net:
+ interface: network-client
+ attr: 2
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ Attrs: map[string]interface{}{"attr": int64(2)},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalSlotsExplicitlyDefinedImplicitlyBoundToApps(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ network-client:
+apps:
+ app:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Check(info.Apps, HasLen, 1)
+ slot := info.Slots["network-client"]
+ app := info.Apps["app"]
+ c.Assert(slot, DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{app.Name: app},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "app",
+ Slots: map[string]*snap.SlotInfo{slot.Name: slot},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalSlotsExplicitlyDefinedExplicitlyBoundToApps(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net: network-client
+apps:
+ app:
+ slots: ["net"]
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Check(info.Apps, HasLen, 1)
+ slot := info.Slots["net"]
+ app := info.Apps["app"]
+ c.Assert(slot, DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "net",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{app.Name: app},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "app",
+ Slots: map[string]*snap.SlotInfo{slot.Name: slot},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalSlotsImplicitlyDefinedExplicitlyBoundToApps(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+apps:
+ app:
+ slots: ["network-client"]
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Check(info.Apps, HasLen, 1)
+ slot := info.Slots["network-client"]
+ app := info.Apps["app"]
+ c.Assert(slot, DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Apps: map[string]*snap.AppInfo{app.Name: app},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "app",
+ Slots: map[string]*snap.SlotInfo{slot.Name: slot},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalSlotWithoutInterfaceName(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ network-client:
+ ipv6-aware: true
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Check(info.Apps, HasLen, 0)
+ c.Assert(info.Slots["network-client"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "network-client",
+ Interface: "network-client",
+ Attrs: map[string]interface{}{"ipv6-aware": true},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalSlotWithLabel(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ led0:
+ interface: bool-file
+ label: Front panel LED (red)
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 1)
+ c.Check(info.Apps, HasLen, 0)
+ c.Assert(info.Slots["led0"], DeepEquals, &snap.SlotInfo{
+ Snap: info,
+ Name: "led0",
+ Interface: "bool-file",
+ Label: "Front panel LED (red)",
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringInterfaceName(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net:
+ interface: 1.0
+ ipv6-aware: true
+`))
+ c.Assert(err, ErrorMatches, `interface name on slot "net" is not a string \(found float64\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringLabel(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ bool-file:
+ label: 1.0
+`))
+ c.Assert(err, ErrorMatches, `label of slot "bool-file" is not a string \(found float64\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringAttributes(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net:
+ 1: ok
+`))
+ c.Assert(err, ErrorMatches, `slot "net" has attribute that is not a string \(found int\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedSlotWithUnexpectedType(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net: 5
+`))
+ c.Assert(err, ErrorMatches, `slot "net" has malformed definition \(found int\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalReservedSlotAttribute(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ serial:
+ interface: serial-port
+ $baud-rate: [9600]
+`))
+ c.Assert(err, ErrorMatches, `slot "serial" uses reserved attribute "\$baud-rate"`)
+}
+
+func (s *YamlSuite) TestUnmarshalInvalidSlotAttribute(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ serial:
+ interface: serial-port
+ foo: null
+`))
+ c.Assert(err, ErrorMatches, `attribute "foo" of slot \"serial\": invalid attribute scalar:.*`)
+}
+
+func (s *YamlSuite) TestUnmarshalHook(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+hooks:
+ test-hook:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 1)
+
+ hook, ok := info.Hooks["test-hook"]
+ c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
+
+ c.Check(hook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "test-hook",
+ Plugs: nil,
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalUnsupportedHook(c *C) {
+ s.restore()
+ hookType := snap.NewHookType(regexp.MustCompile("not-test-hook"))
+ s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
+
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+hooks:
+ test-hook:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 0, Commentf("Expected no hooks to be loaded"))
+}
+
+func (s *YamlSuite) TestUnmarshalHookFiltersOutUnsupportedHooks(c *C) {
+ s.restore()
+ hookType := snap.NewHookType(regexp.MustCompile("test-.*"))
+ s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
+
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+hooks:
+ test-hook:
+ foo-hook:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 0)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 1)
+
+ hook, ok := info.Hooks["test-hook"]
+ c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
+
+ c.Check(hook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "test-hook",
+ Plugs: nil,
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalHookWithPlug(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+hooks:
+ test-hook:
+ plugs: [test-plug]
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 1)
+
+ plug, ok := info.Plugs["test-plug"]
+ c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'"))
+ hook, ok := info.Hooks["test-hook"]
+ c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
+
+ c.Check(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "test-plug",
+ Interface: "test-plug",
+ Hooks: map[string]*snap.HookInfo{hook.Name: hook},
+ })
+ c.Check(hook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "test-hook",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalGlobalPlugsBindToHooks(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ test-plug:
+hooks:
+ test-hook:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 1)
+
+ plug, ok := info.Plugs["test-plug"]
+ c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'"))
+ hook, ok := info.Hooks["test-hook"]
+ c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
+
+ c.Check(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "test-plug",
+ Interface: "test-plug",
+ Hooks: map[string]*snap.HookInfo{hook.Name: hook},
+ })
+ c.Check(hook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "test-hook",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToOneHook(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ test-plug:
+hooks:
+ with-plug:
+ plugs: [test-plug]
+ without-plug:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 2)
+
+ plug := info.Plugs["test-plug"]
+ withPlugHook := info.Hooks["with-plug"]
+ withoutPlugHook := info.Hooks["without-plug"]
+ c.Assert(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "test-plug",
+ Interface: "test-plug",
+ Hooks: map[string]*snap.HookInfo{withPlugHook.Name: withPlugHook},
+ })
+ c.Assert(withPlugHook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "with-plug",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+ c.Assert(withoutPlugHook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "without-plug",
+ Plugs: map[string]*snap.PlugInfo{},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalExplicitGlobalPlugBoundToHook(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ test-plug: test-interface
+hooks:
+ test-hook:
+ plugs: ["test-plug"]
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 0)
+ c.Check(info.Hooks, HasLen, 1)
+
+ plug, ok := info.Plugs["test-plug"]
+ c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'"))
+ hook, ok := info.Hooks["test-hook"]
+ c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
+
+ c.Check(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "test-plug",
+ Interface: "test-interface",
+ Hooks: map[string]*snap.HookInfo{hook.Name: hook},
+ })
+ c.Check(hook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "test-hook",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToHookNotApp(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ test-plug:
+hooks:
+ test-hook:
+ plugs: [test-plug]
+apps:
+ test-app:
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "snap")
+ c.Check(info.Plugs, HasLen, 1)
+ c.Check(info.Slots, HasLen, 0)
+ c.Check(info.Apps, HasLen, 1)
+ c.Check(info.Hooks, HasLen, 1)
+
+ plug := info.Plugs["test-plug"]
+ hook := info.Hooks["test-hook"]
+ app := info.Apps["test-app"]
+ c.Assert(plug, DeepEquals, &snap.PlugInfo{
+ Snap: info,
+ Name: "test-plug",
+ Interface: "test-plug",
+ Apps: map[string]*snap.AppInfo{},
+ Hooks: map[string]*snap.HookInfo{hook.Name: hook},
+ })
+ c.Assert(hook, DeepEquals, &snap.HookInfo{
+ Snap: info,
+ Name: "test-hook",
+ Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
+ })
+ c.Assert(app, DeepEquals, &snap.AppInfo{
+ Snap: info,
+ Name: "test-app",
+ Plugs: map[string]*snap.PlugInfo{},
+ })
+}
+
+func (s *YamlSuite) TestUnmarshalComplexExample(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ info, err := snap.InfoFromSnapYaml([]byte(`
+name: foo
+version: 1.2
+summary: foo app
+type: app
+epoch: 1*
+confinement: devmode
+description: |
+ Foo provides useful services
+apps:
+ daemon:
+ command: foo --daemon
+ plugs: [network, network-bind]
+ slots: [foo-socket-slot]
+ foo:
+ command: fooctl
+ plugs: [foo-socket-plug]
+hooks:
+ test-hook:
+ plugs: [foo-socket-plug]
+plugs:
+ foo-socket-plug:
+ interface: socket
+ # $protocol: foo
+ logging:
+ interface: syslog
+slots:
+ foo-socket-slot:
+ interface: socket
+ path: $SNAP_DATA/socket
+ protocol: foo
+ tracing:
+ interface: ptrace
+`))
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "foo")
+ c.Check(info.Version, Equals, "1.2")
+ c.Check(info.Type, Equals, snap.TypeApp)
+ c.Check(info.Epoch, Equals, "1*")
+ c.Check(info.Confinement, Equals, snap.DevModeConfinement)
+ c.Check(info.Summary(), Equals, "foo app")
+ c.Check(info.Description(), Equals, "Foo provides useful services\n")
+ c.Check(info.Apps, HasLen, 2)
+ c.Check(info.Plugs, HasLen, 4)
+ c.Check(info.Slots, HasLen, 2)
+ // these don't come from snap.yaml
+ c.Check(info.Publisher, Equals, "")
+ c.Check(info.PublisherID, Equals, "")
+ c.Check(info.Channel, Equals, "")
+
+ app1 := info.Apps["daemon"]
+ app2 := info.Apps["foo"]
+ hook := info.Hooks["test-hook"]
+ plug1 := info.Plugs["network"]
+ plug2 := info.Plugs["network-bind"]
+ plug3 := info.Plugs["foo-socket-plug"]
+ plug4 := info.Plugs["logging"]
+ slot1 := info.Slots["foo-socket-slot"]
+ slot2 := info.Slots["tracing"]
+
+ // app1 ("daemon") has three plugs ("network", "network-bind", "logging")
+ // and two slots ("foo-socket", "tracing"). The slot "tracing" and plug
+ // "logging" are global, everything else is app-bound.
+
+ c.Assert(app1, Not(IsNil))
+ c.Check(app1.Snap, Equals, info)
+ c.Check(app1.Name, Equals, "daemon")
+ c.Check(app1.Command, Equals, "foo --daemon")
+ c.Check(app1.Plugs, DeepEquals, map[string]*snap.PlugInfo{
+ plug1.Name: plug1, plug2.Name: plug2, plug4.Name: plug4})
+ c.Check(app1.Slots, DeepEquals, map[string]*snap.SlotInfo{
+ slot1.Name: slot1, slot2.Name: slot2})
+
+ // app2 ("foo") has two plugs ("foo-socket", "logging") and one slot
+ // ("tracing"). The slot "tracing" and plug "logging" are global while
+ // "foo-socket" is app-bound.
+
+ c.Assert(app2, Not(IsNil))
+ c.Check(app2.Snap, Equals, info)
+ c.Check(app2.Name, Equals, "foo")
+ c.Check(app2.Command, Equals, "fooctl")
+ c.Check(app2.Plugs, DeepEquals, map[string]*snap.PlugInfo{
+ plug3.Name: plug3, plug4.Name: plug4})
+ c.Check(app2.Slots, DeepEquals, map[string]*snap.SlotInfo{
+ slot2.Name: slot2})
+
+ // hook1 has two plugs ("foo-socket", "logging"). The plug "logging" is
+ // global while "foo-socket" is hook-bound.
+
+ c.Assert(hook, NotNil)
+ c.Check(hook.Snap, Equals, info)
+ c.Check(hook.Name, Equals, "test-hook")
+ c.Check(hook.Plugs, DeepEquals, map[string]*snap.PlugInfo{
+ plug3.Name: plug3, plug4.Name: plug4})
+
+ // plug1 ("network") is implicitly defined and app-bound to "daemon"
+
+ c.Assert(plug1, Not(IsNil))
+ c.Check(plug1.Snap, Equals, info)
+ c.Check(plug1.Name, Equals, "network")
+ c.Check(plug1.Interface, Equals, "network")
+ c.Check(plug1.Attrs, HasLen, 0)
+ c.Check(plug1.Label, Equals, "")
+ c.Check(plug1.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1})
+
+ // plug2 ("network-bind") is implicitly defined and app-bound to "daemon"
+
+ c.Assert(plug2, Not(IsNil))
+ c.Check(plug2.Snap, Equals, info)
+ c.Check(plug2.Name, Equals, "network-bind")
+ c.Check(plug2.Interface, Equals, "network-bind")
+ c.Check(plug2.Attrs, HasLen, 0)
+ c.Check(plug2.Label, Equals, "")
+ c.Check(plug2.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1})
+
+ // plug3 ("foo-socket") is app-bound to "foo"
+
+ c.Assert(plug3, Not(IsNil))
+ c.Check(plug3.Snap, Equals, info)
+ c.Check(plug3.Name, Equals, "foo-socket-plug")
+ c.Check(plug3.Interface, Equals, "socket")
+ c.Check(plug3.Attrs, HasLen, 0)
+ c.Check(plug3.Label, Equals, "")
+ c.Check(plug3.Apps, DeepEquals, map[string]*snap.AppInfo{app2.Name: app2})
+
+ // plug4 ("logging") is global so it is bound to all apps
+
+ c.Assert(plug4, Not(IsNil))
+ c.Check(plug4.Snap, Equals, info)
+ c.Check(plug4.Name, Equals, "logging")
+ c.Check(plug4.Interface, Equals, "syslog")
+ c.Check(plug4.Attrs, HasLen, 0)
+ c.Check(plug4.Label, Equals, "")
+ c.Check(plug4.Apps, DeepEquals, map[string]*snap.AppInfo{
+ app1.Name: app1, app2.Name: app2})
+
+ // slot1 ("foo-socket") is app-bound to "daemon"
+
+ c.Assert(slot1, Not(IsNil))
+ c.Check(slot1.Snap, Equals, info)
+ c.Check(slot1.Name, Equals, "foo-socket-slot")
+ c.Check(slot1.Interface, Equals, "socket")
+ c.Check(slot1.Attrs, DeepEquals, map[string]interface{}{
+ "protocol": "foo", "path": "$SNAP_DATA/socket"})
+ c.Check(slot1.Label, Equals, "")
+ c.Check(slot1.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1})
+
+ // slot2 ("tracing") is global so it is bound to all apps
+
+ c.Assert(slot2, Not(IsNil))
+ c.Check(slot2.Snap, Equals, info)
+ c.Check(slot2.Name, Equals, "tracing")
+ c.Check(slot2.Interface, Equals, "ptrace")
+ c.Check(slot2.Attrs, HasLen, 0)
+ c.Check(slot2.Label, Equals, "")
+ c.Check(slot2.Apps, DeepEquals, map[string]*snap.AppInfo{
+ app1.Name: app1, app2.Name: app2})
+}
+
+// type and architectures
+
+func (s *YamlSuite) TestSnapYamlTypeDefault(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Type, Equals, snap.TypeApp)
+}
+
+func (s *YamlSuite) TestSnapYamlEpochDefault(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Epoch, Equals, "0")
+}
+
+func (s *YamlSuite) TestSnapYamlConfinementDefault(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Confinement, Equals, snap.StrictConfinement)
+}
+
+func (s *YamlSuite) TestSnapYamlMultipleArchitecturesParsing(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+architectures: [i386, armhf]
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Architectures, DeepEquals, []string{"i386", "armhf"})
+}
+
+func (s *YamlSuite) TestSnapYamlSingleArchitecturesParsing(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+architectures: [i386]
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Architectures, DeepEquals, []string{"i386"})
+}
+
+func (s *YamlSuite) TestSnapYamlAssumesParsing(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+assumes: [feature2, feature1]
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Assumes, DeepEquals, []string{"feature1", "feature2"})
+}
+
+func (s *YamlSuite) TestSnapYamlNoArchitecturesParsing(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Architectures, DeepEquals, []string{"all"})
+}
+
+func (s *YamlSuite) TestSnapYamlBadArchitectureParsing(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+architectures:
+ armhf:
+ no
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, NotNil)
+}
+
+func (s *YamlSuite) TestSnapYamlLicenseParsing(c *C) {
+ y := []byte(`
+name: foo
+version: 1.0
+license-agreement: explicit
+license-version: 12`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.LicenseAgreement, Equals, "explicit")
+ c.Assert(info.LicenseVersion, Equals, "12")
+}
+
+// apps
+
+func (s *YamlSuite) TestSimpleAppExample(c *C) {
+ y := []byte(`name: wat
+version: 42
+apps:
+ cm:
+ command: cm0
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{
+ "cm": {
+ Snap: info,
+ Name: "cm",
+ Command: "cm0",
+ },
+ })
+}
+
+func (s *YamlSuite) TestDaemonEverythingExample(c *C) {
+ y := []byte(`name: wat
+version: 42
+apps:
+ svc:
+ command: svc1
+ description: svc one
+ stop-timeout: 25s
+ daemon: forking
+ stop-command: stop-cmd
+ post-stop-command: post-stop-cmd
+ restart-condition: on-abnormal
+ bus-name: busName
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{
+ "svc": {
+ Snap: info,
+ Name: "svc",
+ Command: "svc1",
+ Daemon: "forking",
+ RestartCond: systemd.RestartOnAbnormal,
+ StopTimeout: timeout.Timeout(25 * time.Second),
+ StopCommand: "stop-cmd",
+ PostStopCommand: "post-stop-cmd",
+ BusName: "busName",
+ },
+ })
+}
+
+func (s *YamlSuite) TestSnapYamlGlobalEnvironment(c *C) {
+ y := []byte(`
+name: foo
+version: 1.0
+environment:
+ foo: bar
+ baz: boom
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Environment, DeepEquals, map[string]string{
+ "foo": "bar",
+ "baz": "boom",
+ })
+}
+
+func (s *YamlSuite) TestSnapYamlPerAppEnvironment(c *C) {
+ y := []byte(`
+name: foo
+version: 1.0
+apps:
+ foo:
+ environment:
+ k1: v1
+ k2: v2
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Apps["foo"].Environment, DeepEquals, map[string]string{
+ "k1": "v1",
+ "k2": "v2",
+ })
+}
+
+// classic confinement
+func (s *YamlSuite) TestClassicConfinement(c *C) {
+ y := []byte(`
+name: foo
+confinement: classic
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Assert(info.Confinement, Equals, snap.ClassicConfinement)
+}
+
+func (s *YamlSuite) TestSnapYamlAliases(c *C) {
+ y := []byte(`
+name: foo
+version: 1.0
+apps:
+ foo:
+ aliases: [foo]
+ bar:
+ aliases: [bar, bar1]
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+
+ c.Check(info.Apps["foo"].Aliases, DeepEquals, []string{"foo"})
+ c.Check(info.Apps["bar"].Aliases, DeepEquals, []string{"bar", "bar1"})
+
+ c.Check(info.Aliases, DeepEquals, map[string]*snap.AppInfo{
+ "foo": info.Apps["foo"],
+ "bar": info.Apps["bar"],
+ "bar1": info.Apps["bar"],
+ })
+}
+
+func (s *YamlSuite) TestSnapYamlAliasesConflict(c *C) {
+ y := []byte(`
+name: foo
+version: 1.0
+apps:
+ foo:
+ aliases: [bar]
+ bar:
+ aliases: [bar]
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `cannot set "bar" as alias for both ("foo" and "bar"|"bar" and "foo")`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/snap/squashfs"
+)
+
+type infoSuite struct {
+ restore func()
+}
+
+var _ = Suite(&infoSuite{})
+
+func (s *infoSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ hookType := snap.NewHookType(regexp.MustCompile(".*"))
+ s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
+}
+
+func (s *infoSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ s.restore()
+}
+
+func (s *infoSuite) TestSideInfoOverrides(c *C) {
+ info := &snap.Info{
+ SuggestedName: "name",
+ OriginalSummary: "summary",
+ OriginalDescription: "desc",
+ }
+
+ info.SideInfo = snap.SideInfo{
+ RealName: "newname",
+ EditedSummary: "fixed summary",
+ EditedDescription: "fixed desc",
+ Revision: snap.R(1),
+ SnapID: "snapidsnapidsnapidsnapidsnapidsn",
+ }
+
+ c.Check(info.Name(), Equals, "newname")
+ c.Check(info.Summary(), Equals, "fixed summary")
+ c.Check(info.Description(), Equals, "fixed desc")
+ c.Check(info.Revision, Equals, snap.R(1))
+ c.Check(info.SnapID, Equals, "snapidsnapidsnapidsnapidsnapidsn")
+}
+
+func (s *infoSuite) TestAppInfoSecurityTag(c *C) {
+ appInfo := &snap.AppInfo{Snap: &snap.Info{SuggestedName: "http"}, Name: "GET"}
+ c.Check(appInfo.SecurityTag(), Equals, "snap.http.GET")
+}
+
+func (s *infoSuite) TestAppInfoWrapperPath(c *C) {
+ info, err := snap.InfoFromSnapYaml([]byte(`name: foo
+apps:
+ foo:
+ bar:
+`))
+ c.Assert(err, IsNil)
+
+ c.Check(info.Apps["bar"].WrapperPath(), Equals, filepath.Join(dirs.SnapBinariesDir, "foo.bar"))
+ c.Check(info.Apps["foo"].WrapperPath(), Equals, filepath.Join(dirs.SnapBinariesDir, "foo"))
+}
+
+func (s *infoSuite) TestAppInfoLauncherCommand(c *C) {
+ dirs.SetRootDir("")
+
+ info, err := snap.InfoFromSnapYaml([]byte(`name: foo
+apps:
+ foo:
+ command: foo-bin
+ bar:
+ command: bar-bin -x
+`))
+ c.Assert(err, IsNil)
+ info.Revision = snap.R(42)
+ c.Check(info.Apps["bar"].LauncherCommand(), Equals, "/usr/bin/snap run foo.bar")
+ c.Check(info.Apps["foo"].LauncherCommand(), Equals, "/usr/bin/snap run foo")
+}
+
+const sampleYaml = `
+name: sample
+version: 1
+apps:
+ app:
+ command: foo
+`
+
+const sampleContents = "SNAP"
+
+func (s *infoSuite) TestReadInfo(c *C) {
+ si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
+
+ snapInfo1 := snaptest.MockSnap(c, sampleYaml, sampleContents, si)
+
+ snapInfo2, err := snap.ReadInfo("sample", si)
+ c.Assert(err, IsNil)
+
+ c.Check(snapInfo2.Name(), Equals, "sample")
+ c.Check(snapInfo2.Revision, Equals, snap.R(42))
+ c.Check(snapInfo2.Summary(), Equals, "esummary")
+
+ c.Check(snapInfo2.Apps["app"].Command, Equals, "foo")
+
+ c.Check(snapInfo2, DeepEquals, snapInfo1)
+}
+
+// makeTestSnap here can also be used to produce broken snaps (differently from snaptest.MakeTestSnapWithFiles)!
+func makeTestSnap(c *C, yaml string) string {
+ tmp := c.MkDir()
+ snapSource := filepath.Join(tmp, "snapsrc")
+
+ err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755)
+ c.Assert(err, IsNil)
+
+ // our regular snap.yaml
+ err = ioutil.WriteFile(filepath.Join(snapSource, "meta", "snap.yaml"), []byte(yaml), 0644)
+ c.Assert(err, IsNil)
+
+ dest := filepath.Join(tmp, "foo.snap")
+ snap := squashfs.New(dest)
+ err = snap.Build(snapSource)
+ c.Assert(err, IsNil)
+
+ return dest
+}
+
+// produce descrs for empty hooks suitable for snaptest.PopulateDir
+func emptyHooks(hookNames ...string) (emptyHooks [][]string) {
+ for _, hookName := range hookNames {
+ emptyHooks = append(emptyHooks, []string{filepath.Join("meta", "hooks", hookName), ""})
+ }
+ return
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFile(c *C) {
+ yaml := `name: foo
+version: 1.0
+type: app
+epoch: 1*
+confinement: devmode`
+ snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "foo")
+ c.Check(info.Version, Equals, "1.0")
+ c.Check(info.Type, Equals, snap.TypeApp)
+ c.Check(info.Revision, Equals, snap.R(0))
+ c.Check(info.Epoch, Equals, "1*")
+ c.Check(info.Confinement, Equals, snap.DevModeConfinement)
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileMissingEpoch(c *C) {
+ yaml := `name: foo
+version: 1.0
+type: app`
+ snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "foo")
+ c.Check(info.Version, Equals, "1.0")
+ c.Check(info.Type, Equals, snap.TypeApp)
+ c.Check(info.Revision, Equals, snap.R(0))
+ c.Check(info.Epoch, Equals, "0") // Defaults to 0
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileWithSideInfo(c *C) {
+ yaml := `name: foo
+version: 1.0
+type: app`
+ snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, &snap.SideInfo{
+ RealName: "baz",
+ Revision: snap.R(42),
+ })
+ c.Assert(err, IsNil)
+ c.Check(info.Name(), Equals, "baz")
+ c.Check(info.Version, Equals, "1.0")
+ c.Check(info.Type, Equals, snap.TypeApp)
+ c.Check(info.Revision, Equals, snap.R(42))
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileValidates(c *C) {
+ yaml := `name: foo.bar
+version: 1.0
+type: app`
+ snapPath := makeTestSnap(c, yaml)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, ErrorMatches, "invalid snap name.*")
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidType(c *C) {
+ yaml := `name: foo
+version: 1.0
+type: foo`
+ snapPath := makeTestSnap(c, yaml)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, ErrorMatches, ".*invalid snap type.*")
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidConfinement(c *C) {
+ yaml := `name: foo
+version: 1.0
+confinement: foo`
+ snapPath := makeTestSnap(c, yaml)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, ErrorMatches, ".*invalid confinement type.*")
+}
+
+func (s *infoSuite) TestAppEnvSimple(c *C) {
+ yaml := `name: foo
+version: 1.0
+type: app
+environment:
+ global-k: global-v
+apps:
+ foo:
+ environment:
+ app-k: app-v
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ env := info.Apps["foo"].Env()
+ sort.Strings(env)
+ c.Check(env, DeepEquals, []string{
+ "app-k=app-v\n",
+ "global-k=global-v\n",
+ })
+}
+
+func (s *infoSuite) TestAppEnvOverrideGlobal(c *C) {
+ yaml := `name: foo
+version: 1.0
+type: app
+environment:
+ global-k: global-v
+ global-and-local: global-v
+apps:
+ foo:
+ environment:
+ app-k: app-v
+ global-and-local: local-v
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yaml))
+ c.Assert(err, IsNil)
+
+ env := info.Apps["foo"].Env()
+ sort.Strings(env)
+ c.Check(env, DeepEquals, []string{
+ "app-k=app-v\n",
+ "global-and-local=local-v\n",
+ "global-k=global-v\n",
+ })
+}
+
+func (s *infoSuite) TestSplitSnapApp(c *C) {
+ for _, t := range []struct {
+ in string
+ out []string
+ }{
+ // normal cases
+ {"foo.bar", []string{"foo", "bar"}},
+ {"foo.bar.baz", []string{"foo", "bar.baz"}},
+ // special case, snapName == appName
+ {"foo", []string{"foo", "foo"}},
+ } {
+ snap, app := snap.SplitSnapApp(t.in)
+ c.Check([]string{snap, app}, DeepEquals, t.out)
+ }
+}
+
+func ExampleSpltiSnapApp() {
+ fmt.Println(snap.SplitSnapApp("hello-world.env"))
+ // Output: hello-world env
+}
+
+func ExampleSpltiSnapAppShort() {
+ fmt.Println(snap.SplitSnapApp("hello-world"))
+ // Output: hello-world hello-world
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidHook(c *C) {
+ yaml := `name: foo
+version: 1.0
+hooks:
+ 123abc:`
+ snapPath := makeTestSnap(c, yaml)
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, ErrorMatches, ".*invalid hook name.*")
+}
+
+func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidImplicitHook(c *C) {
+ yaml := `name: foo
+version: 1.0`
+ snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks("123abc"))
+
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Assert(err, ErrorMatches, ".*invalid hook name.*")
+}
+
+func (s *infoSuite) checkInstalledSnapAndSnapFile(c *C, yaml string, contents string, hooks []string, checker func(c *C, info *snap.Info)) {
+ // First check installed snap
+ sideInfo := &snap.SideInfo{Revision: snap.R(42)}
+ info0 := snaptest.MockSnap(c, yaml, contents, sideInfo)
+ snaptest.PopulateDir(info0.MountDir(), emptyHooks(hooks...))
+ info, err := snap.ReadInfo(info0.Name(), sideInfo)
+ c.Check(err, IsNil)
+ checker(c, info)
+
+ // Now check snap file
+ snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks(hooks...))
+ snapf, err := snap.Open(snapPath)
+ c.Assert(err, IsNil)
+ info, err = snap.ReadInfoFromSnapFile(snapf, nil)
+ c.Check(err, IsNil)
+ checker(c, info)
+}
+
+func (s *infoSuite) TestReadInfoNoHooks(c *C) {
+ yaml := `name: foo
+version: 1.0`
+ s.checkInstalledSnapAndSnapFile(c, yaml, "SNAP", nil, func(c *C, info *snap.Info) {
+ // Verify that no hooks were loaded for this snap
+ c.Check(info.Hooks, HasLen, 0)
+ })
+}
+
+func (s *infoSuite) TestReadInfoSingleImplicitHook(c *C) {
+ yaml := `name: foo
+version: 1.0`
+ s.checkInstalledSnapAndSnapFile(c, yaml, "SNAP", []string{"test-hook"}, func(c *C, info *snap.Info) {
+ // Verify that the `test-hook` hook has now been loaded, and that it has
+ // no associated plugs.
+ c.Check(info.Hooks, HasLen, 1)
+ verifyImplicitHook(c, info, "test-hook")
+ })
+}
+
+func (s *infoSuite) TestReadInfoMultipleImplicitHooks(c *C) {
+ yaml := `name: foo
+version: 1.0`
+ s.checkInstalledSnapAndSnapFile(c, yaml, "SNAP", []string{"foo", "bar"}, func(c *C, info *snap.Info) {
+ // Verify that both hooks have now been loaded, and that neither have any
+ // associated plugs.
+ c.Check(info.Hooks, HasLen, 2)
+ verifyImplicitHook(c, info, "foo")
+ verifyImplicitHook(c, info, "bar")
+ })
+}
+
+func (s *infoSuite) TestReadInfoInvalidImplicitHook(c *C) {
+ hookType := snap.NewHookType(regexp.MustCompile("foo"))
+ s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
+
+ yaml := `name: foo
+version: 1.0`
+ s.checkInstalledSnapAndSnapFile(c, yaml, "SNAP", []string{"foo", "bar"}, func(c *C, info *snap.Info) {
+ // Verify that only foo has been loaded, not bar
+ c.Check(info.Hooks, HasLen, 1)
+ verifyImplicitHook(c, info, "foo")
+ })
+}
+
+func (s *infoSuite) TestReadInfoImplicitAndExplicitHooks(c *C) {
+ yaml := `name: foo
+version: 1.0
+hooks:
+ explicit:
+ plugs: [test-plug]`
+ s.checkInstalledSnapAndSnapFile(c, yaml, "SNAP", []string{"explicit", "implicit"}, func(c *C, info *snap.Info) {
+ // Verify that the `implicit` hook has now been loaded, and that it has
+ // no associated plugs. Also verify that the `explicit` hook is still
+ // valid.
+ c.Check(info.Hooks, HasLen, 2)
+ verifyImplicitHook(c, info, "implicit")
+ verifyExplicitHook(c, info, "explicit", []string{"test-plug"})
+ })
+}
+
+func verifyImplicitHook(c *C, info *snap.Info, hookName string) {
+ hook := info.Hooks[hookName]
+ c.Assert(hook, NotNil, Commentf("Expected hooks to contain %q", hookName))
+ c.Check(hook.Name, Equals, hookName)
+ c.Check(hook.Plugs, IsNil)
+}
+
+func verifyExplicitHook(c *C, info *snap.Info, hookName string, plugNames []string) {
+ hook := info.Hooks[hookName]
+ c.Assert(hook, NotNil, Commentf("Expected hooks to contain %q", hookName))
+ c.Check(hook.Name, Equals, hookName)
+ c.Check(hook.Plugs, HasLen, len(plugNames))
+
+ for _, plugName := range plugNames {
+ // Verify that the HookInfo and PlugInfo point to each other
+ plug := hook.Plugs[plugName]
+ c.Assert(plug, NotNil, Commentf("Expected hook plugs to contain %q", plugName))
+ c.Check(plug.Name, Equals, plugName)
+ c.Check(plug.Hooks, HasLen, 1)
+ hook = plug.Hooks[hookName]
+ c.Assert(hook, NotNil, Commentf("Expected plug to be associated with hook %q", hookName))
+ c.Check(hook.Name, Equals, hookName)
+
+ // Verify also that the hook plug made it into info.Plugs
+ c.Check(info.Plugs[plugName], DeepEquals, plug)
+ }
+}
+
+func (s *infoSuite) TestDirAndFileMethods(c *C) {
+ dirs.SetRootDir("")
+ info := &snap.Info{SuggestedName: "name", SideInfo: snap.SideInfo{Revision: snap.R(1)}}
+ c.Check(info.MountDir(), Equals, fmt.Sprintf("%s/name/1", dirs.SnapMountDir))
+ c.Check(info.MountFile(), Equals, "/var/lib/snapd/snaps/name_1.snap")
+ c.Check(info.HooksDir(), Equals, fmt.Sprintf("%s/name/1/meta/hooks", dirs.SnapMountDir))
+ c.Check(info.DataDir(), Equals, "/var/snap/name/1")
+ c.Check(info.UserDataDir("/home/bob"), Equals, "/home/bob/snap/name/1")
+ c.Check(info.UserCommonDataDir("/home/bob"), Equals, "/home/bob/snap/name/common")
+ c.Check(info.CommonDataDir(), Equals, "/var/snap/name/common")
+ c.Check(info.UserXdgRuntimeDir(12345), Equals, "/run/user/12345/snap.name")
+ // XXX: Those are actually a globs, not directories
+ c.Check(info.DataHomeDir(), Equals, "/home/*/snap/name/1")
+ c.Check(info.CommonDataHomeDir(), Equals, "/home/*/snap/name/common")
+ c.Check(info.XdgRuntimeDirs(), Equals, "/run/user/*/snap.name")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// Keep this in sync between snap and client packages.
+
+type Revision struct {
+ N int
+}
+
+func (r Revision) String() string {
+ if r.N == 0 {
+ return "unset"
+ }
+ if r.N < 0 {
+ return fmt.Sprintf("x%d", -r.N)
+ }
+ return strconv.Itoa(int(r.N))
+}
+
+func (r Revision) Unset() bool {
+ return r.N == 0
+}
+
+func (r Revision) Local() bool {
+ return r.N < 0
+}
+
+func (r Revision) Store() bool {
+ return r.N > 0
+}
+
+func (r Revision) MarshalJSON() ([]byte, error) {
+ return []byte(`"` + r.String() + `"`), nil
+}
+
+func (r *Revision) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var s string
+ if err := unmarshal(&s); err != nil {
+ return err
+ }
+ return r.UnmarshalJSON([]byte(`"` + s + `"`))
+}
+
+func (r Revision) MarshalYAML() (interface{}, error) {
+ return r.String(), nil
+}
+
+func (r *Revision) UnmarshalJSON(data []byte) error {
+ if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' {
+ parsed, err := ParseRevision(string(data[1 : len(data)-1]))
+ if err == nil {
+ *r = parsed
+ return nil
+ }
+ } else {
+ n, err := strconv.ParseInt(string(data), 10, 64)
+ if err == nil {
+ r.N = int(n)
+ return nil
+ }
+ }
+ return fmt.Errorf("invalid snap revision: %q", data)
+}
+
+// ParseRevisions returns the representation in r as a revision.
+// See R for a function more suitable for hardcoded revisions.
+func ParseRevision(s string) (Revision, error) {
+ if s == "unset" {
+ return Revision{}, nil
+ }
+ if s != "" && s[0] == 'x' {
+ i, err := strconv.Atoi(s[1:])
+ if err == nil && i > 0 {
+ return Revision{-i}, nil
+ }
+ }
+ i, err := strconv.Atoi(s)
+ if err == nil && i > 0 {
+ return Revision{i}, nil
+ }
+ return Revision{}, fmt.Errorf("invalid snap revision: %#v", s)
+}
+
+// R returns a Revision given an int or a string.
+// Providing an invalid revision type or value causes a runtime panic.
+// See ParseRevision for a polite function that does not panic.
+func R(r interface{}) Revision {
+ switch r := r.(type) {
+ case string:
+ revision, err := ParseRevision(r)
+ if err != nil {
+ panic(err)
+ }
+ return revision
+ case int:
+ return Revision{r}
+ default:
+ panic(fmt.Errorf("cannot use %v (%T) as a snap revision", r, r))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "encoding/json"
+ "strconv"
+
+ . "gopkg.in/check.v1"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// Keep this in sync between snap and client packages.
+
+type revisionSuite struct{}
+
+var _ = Suite(&revisionSuite{})
+
+func (s revisionSuite) TestString(c *C) {
+ c.Assert(snap.R(0).String(), Equals, "unset")
+ c.Assert(snap.R(10).String(), Equals, "10")
+ c.Assert(snap.R(-9).String(), Equals, "x9")
+}
+
+func (s revisionSuite) TestUnset(c *C) {
+ c.Assert(snap.R(0).Unset(), Equals, true)
+ c.Assert(snap.R(10).Unset(), Equals, false)
+ c.Assert(snap.R(-9).Unset(), Equals, false)
+}
+
+func (s revisionSuite) TestLocal(c *C) {
+ c.Assert(snap.R(0).Local(), Equals, false)
+ c.Assert(snap.R(10).Local(), Equals, false)
+ c.Assert(snap.R(-9).Local(), Equals, true)
+}
+
+func (s revisionSuite) TestStore(c *C) {
+ c.Assert(snap.R(0).Store(), Equals, false)
+ c.Assert(snap.R(10).Store(), Equals, true)
+ c.Assert(snap.R(-9).Store(), Equals, false)
+}
+
+func (s revisionSuite) TestJSON(c *C) {
+ for _, n := range []int{0, 10, -9} {
+ r := snap.R(n)
+ data, err := json.Marshal(snap.R(n))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, `"`+r.String()+`"`)
+
+ var got snap.Revision
+ err = json.Unmarshal(data, &got)
+ c.Assert(err, IsNil)
+ c.Assert(got, Equals, r)
+
+ got = snap.Revision{}
+ err = json.Unmarshal([]byte(strconv.Itoa(r.N)), &got)
+ c.Assert(err, IsNil)
+ c.Assert(got, Equals, r)
+ }
+}
+
+func (s revisionSuite) TestYAML(c *C) {
+ for _, v := range []struct {
+ n int
+ s string
+ }{
+ {0, "unset"},
+ {10, `"10"`},
+ {-9, "x9"},
+ } {
+ r := snap.R(v.n)
+ data, err := yaml.Marshal(snap.R(v.n))
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, v.s+"\n")
+
+ var got snap.Revision
+ err = yaml.Unmarshal(data, &got)
+ c.Assert(err, IsNil)
+ c.Assert(got, Equals, r)
+
+ got = snap.Revision{}
+ err = json.Unmarshal([]byte(strconv.Itoa(r.N)), &got)
+ c.Assert(err, IsNil)
+ c.Assert(got, Equals, r)
+ }
+}
+
+func (s revisionSuite) ParseRevision(c *C) {
+ type testItem struct {
+ s string
+ n int
+ e string
+ }
+
+ var tests = []testItem{{
+ s: "unset",
+ n: 0,
+ }, {
+ s: "x1",
+ n: -1,
+ }, {
+ s: "1",
+ n: 1,
+ }, {
+ s: "x-1",
+ e: `invalid snap revision: "x-1"`,
+ }, {
+ s: "x0",
+ e: `invalid snap revision: "x0"`,
+ }, {
+ s: "-1",
+ e: `invalid snap revision: "-1"`,
+ }, {
+ s: "0",
+ e: `invalid snap revision: "0"`,
+ }}
+
+ for _, test := range tests {
+ r, err := snap.ParseRevision(test.s)
+ if test.e != "" {
+ c.Assert(err.Error(), Equals, test.e)
+ continue
+ }
+ c.Assert(r, Equals, snap.R(test.n))
+ }
+}
+
+func (s *revisionSuite) TestR(c *C) {
+ type testItem struct {
+ v interface{}
+ n int
+ e string
+ }
+
+ var tests = []testItem{{
+ v: 0,
+ n: 0,
+ }, {
+ v: -1,
+ n: -1,
+ }, {
+ v: 1,
+ n: 1,
+ }, {
+ v: "unset",
+ n: 0,
+ }, {
+ v: "x1",
+ n: -1,
+ }, {
+ v: "1",
+ n: 1,
+ }, {
+ v: "x-1",
+ e: `invalid snap revision: "x-1"`,
+ }, {
+ v: "x0",
+ e: `invalid snap revision: "x0"`,
+ }, {
+ v: "-1",
+ e: `invalid snap revision: "-1"`,
+ }, {
+ v: "0",
+ e: `invalid snap revision: "0"`,
+ }, {
+ v: int64(1),
+ e: `cannot use 1 \(int64\) as a snap revision`,
+ }}
+
+ for _, test := range tests {
+ if test.e != "" {
+ f := func() { snap.R(test.v) }
+ c.Assert(f, PanicMatches, test.e)
+ continue
+ }
+
+ c.Assert(snap.R(test.v), Equals, snap.R(test.n))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+// SeedSnap points to a snap in the seed to install, together with
+// assertions (or alone if unasserted is true) it will be used to
+// drive the installation and ultimately set SideInfo/SnapState for it.
+type SeedSnap struct {
+ Name string `yaml:"name"`
+
+ // cross-reference/audit
+ SnapID string `yaml:"snap-id,omitempty"`
+
+ // bits that are orthongonal/not in assertions
+ Channel string `yaml:"channel,omitempty"`
+ DevMode bool `yaml:"devmode,omitempty"`
+
+ Private bool `yaml:"private,omitempty"`
+
+ // no assertions are available in the seed for this snap
+ Unasserted bool `yaml:"unasserted,omitempty"`
+
+ File string `yaml:"file"`
+}
+
+type Seed struct {
+ Snaps []*SeedSnap `yaml:"snaps"`
+}
+
+func ReadSeedYaml(fn string) (*Seed, error) {
+ yamlData, err := ioutil.ReadFile(fn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read seed yaml: %s", fn)
+ }
+
+ var seed Seed
+ if err := yaml.Unmarshal(yamlData, &seed); err != nil {
+ return nil, fmt.Errorf("cannot unmarshal %q: %s", yamlData, err)
+ }
+
+ // validate
+ for _, sn := range seed.Snaps {
+ if strings.Contains(sn.File, "/") {
+ return nil, fmt.Errorf("%q must be a filename, not a path", sn.File)
+ }
+ }
+
+ return &seed, nil
+}
+
+func (seed *Seed) Write(seedFn string) error {
+ data, err := yaml.Marshal(&seed)
+ if err != nil {
+ return err
+ }
+ if err := osutil.AtomicWriteFile(seedFn, data, 0644, 0); err != nil {
+ return err
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+type seedYamlTestSuite struct{}
+
+var _ = Suite(&seedYamlTestSuite{})
+
+var mockSeedYaml = []byte(`
+snaps:
+ - name: foo
+ snap-id: snapidsnapidsnapid
+ channel: stable
+ devmode: true
+ file: foo_1.0_all.snap
+ - name: local
+ unasserted: true
+ file: local.snap
+`)
+
+func (s *seedYamlTestSuite) TestSimple(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, mockSeedYaml, 0644)
+ c.Assert(err, IsNil)
+
+ seed, err := snap.ReadSeedYaml(fn)
+ c.Assert(err, IsNil)
+ c.Assert(seed.Snaps, HasLen, 2)
+ c.Assert(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ File: "foo_1.0_all.snap",
+ Name: "foo",
+ SnapID: "snapidsnapidsnapid",
+
+ Channel: "stable",
+ DevMode: true,
+ })
+ c.Assert(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ File: "local.snap",
+ Name: "local",
+ Unasserted: true,
+ })
+}
+
+var badMockSeedYaml = []byte(`
+snaps:
+ - name: foo
+ file: foo/bar.snap
+`)
+
+func (s *seedYamlTestSuite) TestNoPathAllowed(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, badMockSeedYaml, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.ReadSeedYaml(fn)
+ c.Assert(err, ErrorMatches, `"foo/bar.snap" must be a filename, not a path`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapdir
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+)
+
+// SnapDir is the snapdir based snap.
+type SnapDir struct {
+ path string
+}
+
+// Path returns the path of the backing container.
+func (s *SnapDir) Path() string {
+ return s.path
+}
+
+// New returns a new snap directory container.
+func New(path string) *SnapDir {
+ return &SnapDir{path: path}
+}
+
+func (s *SnapDir) Size() (size int64, err error) {
+ totalSize := int64(0)
+ f := func(_ string, info os.FileInfo, err error) error {
+ totalSize += info.Size()
+ return err
+ }
+ filepath.Walk(s.path, f)
+
+ return totalSize, nil
+}
+
+func (s *SnapDir) Install(targetPath, mountDir string) error {
+ return os.Symlink(s.path, targetPath)
+}
+
+func (s *SnapDir) ReadFile(file string) (content []byte, err error) {
+ return ioutil.ReadFile(filepath.Join(s.path, file))
+}
+
+func (s *SnapDir) ListDir(path string) ([]string, error) {
+ fileInfos, err := ioutil.ReadDir(filepath.Join(s.path, path))
+ if err != nil {
+ return nil, err
+ }
+
+ var fileNames []string
+ for _, fileInfo := range fileInfos {
+ fileNames = append(fileNames, fileInfo.Name())
+ }
+
+ return fileNames, nil
+}
+
+func (s *SnapDir) Unpack(src, dstDir string) error {
+ return fmt.Errorf("unpack is not supported with snaps of type snapdir")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapdir_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/snapcore/snapd/snap/snapdir"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type SnapdirTestSuite struct {
+}
+
+var _ = Suite(&SnapdirTestSuite{})
+
+func (s *SnapdirTestSuite) TestReadFile(c *C) {
+ d := c.MkDir()
+ needle := []byte(`stuff`)
+ err := ioutil.WriteFile(filepath.Join(d, "foo"), needle, 0644)
+ c.Assert(err, IsNil)
+
+ snap := snapdir.New(d)
+ content, err := snap.ReadFile("foo")
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, needle)
+}
+
+func (s *SnapdirTestSuite) TestListDir(c *C) {
+ d := c.MkDir()
+
+ err := os.MkdirAll(filepath.Join(d, "test"), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(d, "test", "test1"), nil, 0644)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(d, "test", "test2"), nil, 0644)
+ c.Assert(err, IsNil)
+
+ snap := snapdir.New(d)
+ fileNames, err := snap.ListDir("test")
+ c.Assert(err, IsNil)
+ c.Assert(fileNames, HasLen, 2)
+ c.Check(fileNames[0], Equals, "test1")
+ c.Check(fileNames[1], Equals, "test2")
+}
+
+func (s *SnapdirTestSuite) TestInstall(c *C) {
+ tryBaseDir := c.MkDir()
+ snap := snapdir.New(tryBaseDir)
+
+ varLibSnapd := c.MkDir()
+ targetPath := filepath.Join(varLibSnapd, "foo_1.0.snap")
+ err := snap.Install(targetPath, "unused-mount-dir")
+ c.Assert(err, IsNil)
+ symlinkTarget, err := filepath.EvalSymlinks(targetPath)
+ c.Assert(err, IsNil)
+ c.Assert(symlinkTarget, Equals, tryBaseDir)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapenv
+
+import (
+ "fmt"
+ "os"
+ "os/user"
+ "strings"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/snap"
+)
+
+// ExecEnv returns the full environment that is required for
+// snap-{confine,exec}(like SNAP_{NAME,REVISION} etc are all set).
+//
+// It merges it with the existing os.Environ() and ensures the SNAP_*
+// overrides the any pre-existing environment variables.
+func ExecEnv(info *snap.Info) []string {
+ // merge environment and the snap environment, note that the
+ // snap environment overrides pre-existing env entries
+ env := envMap(os.Environ())
+ snapEnv := snapEnv(info)
+ for k, v := range snapEnv {
+ env[k] = v
+ }
+ return envFromMap(env)
+}
+
+// snapEnv returns the extra environment that is required for
+// snap-{confine,exec} to work.
+func snapEnv(info *snap.Info) map[string]string {
+ var home string
+
+ usr, err := user.Current()
+ if err == nil {
+ home = usr.HomeDir
+ }
+
+ env := basicEnv(info)
+ if home != "" {
+ for k, v := range userEnv(info, home) {
+ env[k] = v
+ }
+ }
+ return env
+}
+
+// basicEnv returns the app-level environment variables for a snap.
+// Despite this being a bit snap-specific, this is in helpers.go because it's
+// used by so many other modules, we run into circular dependencies if it's
+// somewhere more reasonable like the snappy module.
+func basicEnv(info *snap.Info) map[string]string {
+ return map[string]string{
+ "SNAP": info.MountDir(),
+ "SNAP_COMMON": info.CommonDataDir(),
+ "SNAP_DATA": info.DataDir(),
+ "SNAP_NAME": info.Name(),
+ "SNAP_VERSION": info.Version,
+ "SNAP_REVISION": info.Revision.String(),
+ "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:",
+ "SNAP_REEXEC": os.Getenv("SNAP_REEXEC"),
+ }
+}
+
+// userEnv returns the user-level environment variables for a snap.
+// Despite this being a bit snap-specific, this is in helpers.go because it's
+// used by so many other modules, we run into circular dependencies if it's
+// somewhere more reasonable like the snappy module.
+func userEnv(info *snap.Info, home string) map[string]string {
+ result := map[string]string{
+ "SNAP_USER_COMMON": info.UserCommonDataDir(home),
+ "SNAP_USER_DATA": info.UserDataDir(home),
+ "XDG_RUNTIME_DIR": info.UserXdgRuntimeDir(os.Geteuid()),
+ }
+ // For non-classic snaps, we set HOME but on classic allow snaps to see real HOME
+ if !info.NeedsClassic() {
+ result["HOME"] = info.UserDataDir(home)
+ }
+ return result
+}
+
+// envMap creates a map from the given environment string list, e.g. the
+// list returned from os.Environ()
+func envMap(env []string) map[string]string {
+ envMap := map[string]string{}
+ for _, kv := range env {
+ l := strings.SplitN(kv, "=", 2)
+ if len(l) < 2 {
+ continue // strange
+ }
+ k, v := l[0], l[1]
+ envMap[k] = v
+ }
+ return envMap
+}
+
+// envFromMap creates a list of strings of the form k=v from a dict. This is
+// useful in combination with envMap to create an environment suitable to
+// pass to e.g. syscall.Exec()
+func envFromMap(em map[string]string) []string {
+ var out []string
+ for k, v := range em {
+ out = append(out, fmt.Sprintf("%s=%s", k, v))
+ }
+ return out
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snapenv
+
+import (
+ "fmt"
+ "os"
+ "os/user"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type HTestSuite struct{}
+
+var _ = Suite(&HTestSuite{})
+
+var mockYaml = []byte(`name: snapname
+version: 1.0
+apps:
+ app:
+ command: run-app
+hooks:
+ configure:
+`)
+
+var mockSnapInfo = &snap.Info{
+ SuggestedName: "foo",
+ Version: "1.0",
+ SideInfo: snap.SideInfo{
+ Revision: snap.R(17),
+ },
+}
+var mockClassicSnapInfo = &snap.Info{
+ SuggestedName: "foo",
+ Version: "1.0",
+ SideInfo: snap.SideInfo{
+ Revision: snap.R(17),
+ },
+ Confinement: snap.ClassicConfinement,
+}
+
+func (ts *HTestSuite) TestBasic(c *C) {
+ env := basicEnv(mockSnapInfo)
+
+ c.Assert(env, DeepEquals, map[string]string{
+ "SNAP": fmt.Sprintf("%s/foo/17", dirs.SnapMountDir),
+ "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_COMMON": "/var/snap/foo/common",
+ "SNAP_DATA": "/var/snap/foo/17",
+ "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:",
+ "SNAP_NAME": "foo",
+ "SNAP_REEXEC": "",
+ "SNAP_REVISION": "17",
+ "SNAP_VERSION": "1.0",
+ })
+
+}
+
+func (ts *HTestSuite) TestUser(c *C) {
+ env := userEnv(mockSnapInfo, "/root")
+
+ c.Assert(env, DeepEquals, map[string]string{
+ "HOME": "/root/snap/foo/17",
+ "SNAP_USER_COMMON": "/root/snap/foo/common",
+ "SNAP_USER_DATA": "/root/snap/foo/17",
+ "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo", os.Geteuid()),
+ })
+}
+
+func (ts *HTestSuite) TestUserForClassicConfinement(c *C) {
+ env := userEnv(mockClassicSnapInfo, "/root")
+
+ c.Assert(env, DeepEquals, map[string]string{
+ // NOTE HOME Is absent! we no longer override it
+ "SNAP_USER_COMMON": "/root/snap/foo/common",
+ "SNAP_USER_DATA": "/root/snap/foo/17",
+ "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo", os.Geteuid()),
+ })
+}
+
+func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, IsNil)
+ info.SideInfo.Revision = snap.R(42)
+
+ usr, err := user.Current()
+ c.Assert(err, IsNil)
+
+ homeEnv := os.Getenv("HOME")
+ defer os.Setenv("HOME", homeEnv)
+
+ for _, withHomeEnv := range []bool{true, false} {
+ if !withHomeEnv {
+ os.Setenv("HOME", "")
+ }
+
+ env := snapEnv(info)
+ c.Check(env, DeepEquals, map[string]string{
+ "HOME": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir),
+ "SNAP": fmt.Sprintf("%s/snapname/42", dirs.SnapMountDir),
+ "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_COMMON": "/var/snap/snapname/common",
+ "SNAP_DATA": "/var/snap/snapname/42",
+ "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:",
+ "SNAP_NAME": "snapname",
+ "SNAP_REEXEC": "",
+ "SNAP_REVISION": "42",
+ "SNAP_USER_COMMON": fmt.Sprintf("%s/snap/snapname/common", usr.HomeDir),
+ "SNAP_USER_DATA": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir),
+ "SNAP_VERSION": "1.0",
+ "XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.snapname", os.Geteuid()),
+ })
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snaptest
+
+// TODO: replace this using some subset from snapcraft or simplify further!
+
+import (
+ "bufio"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "syscall"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/squashfs"
+)
+
+// from click's click.build.ClickBuilderBase, and there from
+// @Dpkg::Source::Package::tar_ignore_default_pattern;
+// changed to regexps from globs for sanity (hah)
+//
+// Please resist the temptation of optimizing the regexp by grouping
+// things by hand. People will find it unreadable enough as it is.
+var shouldExcludeDefault = regexp.MustCompile(strings.Join([]string{
+ `\.snap$`, // added
+ `\.click$`,
+ `^\..*\.sw.$`,
+ `~$`,
+ `^,,`,
+ `^\.[#~]`,
+ `^\.arch-ids$`,
+ `^\.arch-inventory$`,
+ `^\.bzr$`,
+ `^\.bzr-builddeb$`,
+ `^\.bzr\.backup$`,
+ `^\.bzr\.tags$`,
+ `^\.bzrignore$`,
+ `^\.cvsignore$`,
+ `^\.git$`,
+ `^\.gitattributes$`,
+ `^\.gitignore$`,
+ `^\.gitmodules$`,
+ `^\.hg$`,
+ `^\.hgignore$`,
+ `^\.hgsigs$`,
+ `^\.hgtags$`,
+ `^\.shelf$`,
+ `^\.svn$`,
+ `^CVS$`,
+ `^DEADJOE$`,
+ `^RCS$`,
+ `^_MTN$`,
+ `^_darcs$`,
+ `^{arch}$`,
+ `^\.snapignore$`,
+}, "|")).MatchString
+
+// fake static function variables
+type keep struct {
+ basedir string
+ exclude func(string) bool
+}
+
+func (k *keep) shouldExclude(basedir string, file string) bool {
+ if basedir == k.basedir {
+ if k.exclude == nil {
+ return false
+ }
+
+ return k.exclude(file)
+ }
+
+ k.basedir = basedir
+ k.exclude = nil
+
+ snapignore, err := os.Open(filepath.Join(basedir, ".snapignore"))
+ if err != nil {
+ return false
+ }
+
+ scanner := bufio.NewScanner(snapignore)
+ var lines []string
+ for scanner.Scan() {
+ line := scanner.Text()
+ if _, err := regexp.Compile(line); err != nil {
+ // not a regexp
+ line = regexp.QuoteMeta(line)
+ }
+ lines = append(lines, line)
+ }
+
+ fullRegex := strings.Join(lines, "|")
+ exclude, err := regexp.Compile(fullRegex)
+ if err == nil {
+ k.exclude = exclude.MatchString
+
+ return k.exclude(file)
+ }
+
+ // can't happen; can't even find a way to trigger it in testing.
+ panic(fmt.Sprintf("|-composition of valid regexps is invalid?!? Please report this bug: %#v", fullRegex))
+}
+
+var shouldExcludeDynamic = new(keep).shouldExclude
+
+func shouldExclude(basedir string, file string) bool {
+ return shouldExcludeDefault(file) || shouldExcludeDynamic(basedir, file)
+}
+
+// small helper that return the architecture or "multi" if its multiple arches
+func debArchitecture(info *snap.Info) string {
+ switch len(info.Architectures) {
+ case 0:
+ return "unknown"
+ case 1:
+ return info.Architectures[0]
+ default:
+ return "multi"
+ }
+}
+
+func copyToBuildDir(sourceDir, buildDir string) error {
+ sourceDir, err := filepath.Abs(sourceDir)
+ if err != nil {
+ return err
+ }
+
+ err = os.Remove(buildDir)
+ if err != nil && !os.IsNotExist(err) {
+ // this shouldn't happen, but.
+ return err
+ }
+
+ // no umask here so that we get the permissions correct
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
+ return filepath.Walk(sourceDir, func(path string, info os.FileInfo, errin error) (err error) {
+ if errin != nil {
+ return errin
+ }
+
+ relpath := path[len(sourceDir):]
+ if relpath == "/DEBIAN" || shouldExclude(sourceDir, filepath.Base(path)) {
+ if info.IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ dest := filepath.Join(buildDir, relpath)
+
+ // handle dirs
+ if info.IsDir() {
+ if err := os.Mkdir(dest, info.Mode()); err != nil {
+ return err
+ }
+ // ensure that permissions are preserved
+ uid := int(info.Sys().(*syscall.Stat_t).Uid)
+ gid := int(info.Sys().(*syscall.Stat_t).Gid)
+ return os.Chown(dest, uid, gid)
+ }
+
+ // handle char/block devices
+ if osutil.IsDevice(info.Mode()) {
+ return osutil.CopySpecialFile(path, dest)
+ }
+
+ if (info.Mode() & os.ModeSymlink) != 0 {
+ target, err := os.Readlink(path)
+ if err != nil {
+ return err
+ }
+ return os.Symlink(target, dest)
+ }
+
+ // fail if its unsupported
+ if !info.Mode().IsRegular() {
+ return fmt.Errorf("cannot handle type of file %s", path)
+ }
+
+ // it's a file. Maybe we can link it?
+ if os.Link(path, dest) == nil {
+ // whee
+ return nil
+ }
+ // sigh. ok, copy it is.
+ return osutil.CopyFile(path, dest, osutil.CopyFlagDefault)
+ })
+}
+
+func prepare(sourceDir, targetDir, buildDir string) (snapName string, err error) {
+ // ensure we have valid content
+ yaml, err := ioutil.ReadFile(filepath.Join(sourceDir, "meta", "snap.yaml"))
+ if err != nil {
+ return "", err
+ }
+
+ info, err := snap.InfoFromSnapYaml(yaml)
+ if err != nil {
+ return "", err
+ }
+
+ err = snap.Validate(info)
+ if err != nil {
+ return "", err
+ }
+
+ if err := copyToBuildDir(sourceDir, buildDir); err != nil {
+ return "", err
+ }
+
+ // build the package
+ snapName = fmt.Sprintf("%s_%s_%v.snap", info.Name(), info.Version, debArchitecture(info))
+
+ if targetDir != "" {
+ snapName = filepath.Join(targetDir, snapName)
+ if _, err := os.Stat(targetDir); os.IsNotExist(err) {
+ if err := os.MkdirAll(targetDir, 0755); err != nil {
+ return "", err
+ }
+ }
+ }
+
+ return snapName, nil
+}
+
+// BuildSquashfsSnap the given sourceDirectory and return the generated
+// snap file
+func BuildSquashfsSnap(sourceDir, targetDir string) (string, error) {
+ // create build dir
+ buildDir, err := ioutil.TempDir("", "snappy-build-")
+ if err != nil {
+ return "", err
+ }
+ defer os.RemoveAll(buildDir)
+
+ snapName, err := prepare(sourceDir, targetDir, buildDir)
+ if err != nil {
+ return "", err
+ }
+
+ d := squashfs.New(snapName)
+ if err = d.Build(buildDir); err != nil {
+ return "", err
+ }
+
+ return snapName, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snaptest_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "syscall"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+
+ . "gopkg.in/check.v1"
+)
+
+type BuildTestSuite struct {
+ testutil.BaseTest
+}
+
+var _ = Suite(&BuildTestSuite{})
+
+func (s *BuildTestSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+
+ // chdir into a tempdir
+ pwd, err := os.Getwd()
+ c.Assert(err, IsNil)
+ s.AddCleanup(func() { os.Chdir(pwd) })
+ err = os.Chdir(c.MkDir())
+ c.Assert(err, IsNil)
+
+ // use fake root
+ dirs.SetRootDir(c.MkDir())
+}
+
+func makeExampleSnapSourceDir(c *C, snapYamlContent string) string {
+ tempdir := c.MkDir()
+
+ // use meta/snap.yaml
+ metaDir := filepath.Join(tempdir, "meta")
+ err := os.Mkdir(metaDir, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(snapYamlContent), 0644)
+ c.Assert(err, IsNil)
+
+ const helloBinContent = `#!/bin/sh
+printf "hello world"
+`
+
+ // an example binary
+ binDir := filepath.Join(tempdir, "bin")
+ err = os.Mkdir(binDir, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(binDir, "hello-world"), []byte(helloBinContent), 0755)
+ c.Assert(err, IsNil)
+
+ // unusual permissions for dir
+ tmpDir := filepath.Join(tempdir, "tmp")
+ err = os.Mkdir(tmpDir, 0755)
+ c.Assert(err, IsNil)
+ // avoid umask
+ err = os.Chmod(tmpDir, 01777)
+ c.Assert(err, IsNil)
+
+ // and file
+ someFile := filepath.Join(tempdir, "file-with-perm")
+ err = ioutil.WriteFile(someFile, []byte(""), 0666)
+ c.Assert(err, IsNil)
+ err = os.Chmod(someFile, 0666)
+ c.Assert(err, IsNil)
+
+ // an example symlink
+ err = os.Symlink("bin/hello-world", filepath.Join(tempdir, "symlink"))
+ c.Assert(err, IsNil)
+
+ return tempdir
+}
+
+func (s *BuildTestSuite) TestBuildNoManifestFails(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, "")
+ c.Assert(os.Remove(filepath.Join(sourceDir, "meta", "snap.yaml")), IsNil)
+ _, err := snaptest.BuildSquashfsSnap(sourceDir, "")
+ c.Assert(err, NotNil) // XXX maybe make the error more explicit
+}
+
+func (s *BuildTestSuite) TestCopyCopies(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, "name: hello")
+ // actually this'll be on /tmp so it'll be a link
+ target := c.MkDir()
+ c.Assert(snaptest.CopyToBuildDir(sourceDir, target), IsNil)
+ out, err := exec.Command("diff", "-qrN", sourceDir, target).Output()
+ c.Check(err, IsNil)
+ c.Check(out, DeepEquals, []byte{})
+}
+
+func (s *BuildTestSuite) TestCopyActuallyCopies(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, "name: hello")
+
+ // hoping to get the non-linking behaviour via /dev/shm
+ target, err := ioutil.TempDir("/dev/shm", "copy")
+ // sbuild environments won't allow writing to /dev/shm, so its
+ // ok to skip there
+ if os.IsPermission(err) {
+ c.Skip("/dev/shm is not writable for us")
+ }
+ c.Assert(err, IsNil)
+
+ c.Assert(snaptest.CopyToBuildDir(sourceDir, target), IsNil)
+ out, err := exec.Command("diff", "-qrN", sourceDir, target).Output()
+ c.Check(err, IsNil)
+ c.Check(out, DeepEquals, []byte{})
+}
+
+func (s *BuildTestSuite) TestCopyExcludesBackups(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, "name: hello")
+ target := c.MkDir()
+ // add a backup file
+ c.Assert(ioutil.WriteFile(filepath.Join(sourceDir, "foo~"), []byte("hi"), 0755), IsNil)
+ c.Assert(snaptest.CopyToBuildDir(sourceDir, target), IsNil)
+ cmd := exec.Command("diff", "-qr", sourceDir, target)
+ cmd.Env = append(cmd.Env, "LANG=C")
+ out, err := cmd.Output()
+ c.Check(err, NotNil)
+ c.Check(string(out), Matches, `(?m)Only in \S+: foo~`)
+}
+
+func (s *BuildTestSuite) TestCopyExcludesTopLevelDEBIAN(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, "name: hello")
+ target := c.MkDir()
+ // add a toplevel DEBIAN
+ c.Assert(os.MkdirAll(filepath.Join(sourceDir, "DEBIAN", "foo"), 0755), IsNil)
+ // and a non-toplevel DEBIAN
+ c.Assert(os.MkdirAll(filepath.Join(sourceDir, "bar", "DEBIAN", "baz"), 0755), IsNil)
+ c.Assert(snaptest.CopyToBuildDir(sourceDir, target), IsNil)
+ cmd := exec.Command("diff", "-qr", sourceDir, target)
+ cmd.Env = append(cmd.Env, "LANG=C")
+ out, err := cmd.Output()
+ c.Check(err, NotNil)
+ c.Check(string(out), Matches, `(?m)Only in \S+: DEBIAN`)
+ // but *only one* DEBIAN is skipped
+ c.Check(strings.Count(string(out), "Only in"), Equals, 1)
+}
+
+func (s *BuildTestSuite) TestCopyExcludesWholeDirs(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, "name: hello")
+ target := c.MkDir()
+ // add a file inside a skipped dir
+ c.Assert(os.Mkdir(filepath.Join(sourceDir, ".bzr"), 0755), IsNil)
+ c.Assert(ioutil.WriteFile(filepath.Join(sourceDir, ".bzr", "foo"), []byte("hi"), 0755), IsNil)
+ c.Assert(snaptest.CopyToBuildDir(sourceDir, target), IsNil)
+ out, _ := exec.Command("find", sourceDir).Output()
+ c.Check(string(out), Not(Equals), "")
+ cmd := exec.Command("diff", "-qr", sourceDir, target)
+ cmd.Env = append(cmd.Env, "LANG=C")
+ out, err := cmd.Output()
+ c.Check(err, NotNil)
+ c.Check(string(out), Matches, `(?m)Only in \S+: \.bzr`)
+}
+
+func (s *BuildTestSuite) TestExcludeDynamicFalseIfNoSnapignore(c *C) {
+ basedir := c.MkDir()
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "foo"), Equals, false)
+}
+
+func (s *BuildTestSuite) TestExcludeDynamicWorksIfSnapignore(c *C) {
+ basedir := c.MkDir()
+ c.Assert(ioutil.WriteFile(filepath.Join(basedir, ".snapignore"), []byte("foo\nb.r\n"), 0644), IsNil)
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "foo"), Equals, true)
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "bar"), Equals, true)
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "bzr"), Equals, true)
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "baz"), Equals, false)
+}
+
+func (s *BuildTestSuite) TestExcludeDynamicWeirdRegexps(c *C) {
+ basedir := c.MkDir()
+ c.Assert(ioutil.WriteFile(filepath.Join(basedir, ".snapignore"), []byte("*hello\n"), 0644), IsNil)
+ // note "*hello" is not a valid regexp, so will be taken literally (not globbed!)
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "ahello"), Equals, false)
+ c.Check(snaptest.ShouldExcludeDynamic(basedir, "*hello"), Equals, true)
+}
+
+func (s *BuildTestSuite) TestDebArchitecture(c *C) {
+ c.Check(snaptest.DebArchitecture(&snap.Info{Architectures: []string{"foo"}}), Equals, "foo")
+ c.Check(snaptest.DebArchitecture(&snap.Info{Architectures: []string{"foo", "bar"}}), Equals, "multi")
+ c.Check(snaptest.DebArchitecture(&snap.Info{Architectures: nil}), Equals, "unknown")
+}
+
+func (s *BuildTestSuite) TestBuildFailsForUnknownType(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, `name: hello
+version: 1.0.1
+`)
+ err := syscall.Mkfifo(filepath.Join(sourceDir, "fifo"), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snaptest.BuildSquashfsSnap(sourceDir, "")
+ c.Assert(err, ErrorMatches, "cannot handle type of file .*")
+}
+
+func (s *BuildTestSuite) TestBuildSquashfsSimple(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, `name: hello
+version: 1.0.1
+architectures: ["i386", "amd64"]
+integration:
+ app:
+ apparmor-profile: meta/hello.apparmor
+`)
+
+ resultSnap, err := snaptest.BuildSquashfsSnap(sourceDir, "")
+ c.Assert(err, IsNil)
+
+ // check that there is result
+ _, err = os.Stat(resultSnap)
+ c.Assert(err, IsNil)
+ c.Assert(resultSnap, Equals, "hello_1.0.1_multi.snap")
+
+ // check that the content looks sane
+ output, err := exec.Command("unsquashfs", "-ll", "hello_1.0.1_multi.snap").CombinedOutput()
+ c.Assert(err, IsNil)
+ for _, needle := range []string{
+ "meta/snap.yaml",
+ "bin/hello-world",
+ "symlink -> bin/hello-world",
+ } {
+ expr := fmt.Sprintf(`(?ms).*%s.*`, regexp.QuoteMeta(needle))
+ c.Assert(string(output), Matches, expr)
+ }
+}
+
+func (s *BuildTestSuite) TestBuildSimpleOutputDir(c *C) {
+ sourceDir := makeExampleSnapSourceDir(c, `name: hello
+version: 1.0.1
+architectures: ["i386", "amd64"]
+integration:
+ app:
+ apparmor-profile: meta/hello.apparmor
+`)
+
+ outputDir := filepath.Join(c.MkDir(), "output")
+ snapOutput := filepath.Join(outputDir, "hello_1.0.1_multi.snap")
+ resultSnap, err := snaptest.BuildSquashfsSnap(sourceDir, outputDir)
+ c.Assert(err, IsNil)
+
+ // check that there is result
+ _, err = os.Stat(resultSnap)
+ c.Assert(err, IsNil)
+ c.Assert(resultSnap, Equals, snapOutput)
+
+ // check that the content looks sane
+ output, err := exec.Command("unsquashfs", "-ll", resultSnap).CombinedOutput()
+ c.Assert(err, IsNil)
+ for _, needle := range []string{
+ "meta/snap.yaml",
+ "bin/hello-world",
+ "symlink -> bin/hello-world",
+ } {
+ expr := fmt.Sprintf(`(?ms).*%s.*`, regexp.QuoteMeta(needle))
+ c.Assert(string(output), Matches, expr)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snaptest
+
+var (
+ CopyToBuildDir = copyToBuildDir
+ ShouldExcludeDynamic = shouldExcludeDynamic
+ DebArchitecture = debArchitecture
+)
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package snaptest contains helper functions for mocking snaps.
+package snaptest
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// MockSnap puts a snap.yaml file on disk so to mock an installed snap, based on the provided arguments.
+//
+// The caller is responsible for mocking root directory with dirs.SetRootDir()
+// and for altering the overlord state if required.
+func MockSnap(c *check.C, yamlText string, snapContents string, sideInfo *snap.SideInfo) *snap.Info {
+ c.Assert(sideInfo, check.Not(check.IsNil))
+
+ // Parse the yaml (we need the Name).
+ snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText))
+ c.Assert(err, check.IsNil)
+
+ // Set SideInfo so that we can use MountDir below
+ snapInfo.SideInfo = *sideInfo
+
+ // Put the YAML on disk, in the right spot.
+ metaDir := filepath.Join(snapInfo.MountDir(), "meta")
+ err = os.MkdirAll(metaDir, 0755)
+ c.Assert(err, check.IsNil)
+ err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(yamlText), 0644)
+ c.Assert(err, check.IsNil)
+
+ // Write the .snap to disk
+ err = os.MkdirAll(filepath.Dir(snapInfo.MountFile()), 0755)
+ c.Assert(err, check.IsNil)
+ err = ioutil.WriteFile(snapInfo.MountFile(), []byte(snapContents), 0644)
+ c.Assert(err, check.IsNil)
+ snapInfo.Size = int64(len(snapContents))
+
+ return snapInfo
+}
+
+// MockInfo parses the given snap.yaml text and returns a validated snap.Info object including the optional SideInfo.
+//
+// The result is just kept in memory, there is nothing kept on disk. If that is
+// desired please use MockSnap instead.
+func MockInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
+ if sideInfo == nil {
+ sideInfo = &snap.SideInfo{}
+ }
+
+ snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText))
+ c.Assert(err, check.IsNil)
+ snapInfo.SideInfo = *sideInfo
+ err = snap.Validate(snapInfo)
+ c.Assert(err, check.IsNil)
+ return snapInfo
+}
+
+// PopulateDir populates the directory with files specified as pairs of relative file path and its content. Useful to add extra files to a snap.
+func PopulateDir(dir string, files [][]string) {
+ for _, filenameAndContent := range files {
+ filename := filenameAndContent[0]
+ content := filenameAndContent[1]
+ fpath := filepath.Join(dir, filename)
+ err := os.MkdirAll(filepath.Dir(fpath), 0755)
+ if err != nil {
+ panic(err)
+ }
+ err = ioutil.WriteFile(fpath, []byte(content), 0644)
+ if err != nil {
+ panic(err)
+ }
+ }
+}
+
+// MakeTestSnapWithFiles makes a squashfs snap file with the given
+// snap.yaml content and optional extras files specified as pairs of
+// relative file path and its content.
+func MakeTestSnapWithFiles(c *check.C, snapYamlContent string, files [][]string) (snapFilePath string) {
+ tmpdir := c.MkDir()
+ snapSource := filepath.Join(tmpdir, "snapsrc")
+
+ err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755)
+ if err != nil {
+ panic(err)
+ }
+ snapYamlFn := filepath.Join(snapSource, "meta", "snap.yaml")
+ err = ioutil.WriteFile(snapYamlFn, []byte(snapYamlContent), 0644)
+ if err != nil {
+ panic(err)
+ }
+
+ PopulateDir(snapSource, files)
+
+ err = osutil.ChDir(snapSource, func() error {
+ var err error
+ snapFilePath, err = BuildSquashfsSnap(snapSource, "")
+ return err
+ })
+ if err != nil {
+ panic(err)
+ }
+ return filepath.Join(snapSource, snapFilePath)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snaptest_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+func TestSnapTest(t *testing.T) { TestingT(t) }
+
+const sampleYaml = `
+name: sample
+version: 1
+apps:
+ app:
+ command: foo
+plugs:
+ network:
+ interface: network
+`
+const sampleContents = ""
+
+type snapTestSuite struct{}
+
+var _ = Suite(&snapTestSuite{})
+
+func (s *snapTestSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *snapTestSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *snapTestSuite) TestMockSnap(c *C) {
+ snapInfo := snaptest.MockSnap(c, sampleYaml, sampleContents, &snap.SideInfo{Revision: snap.R(42)})
+ // Data from YAML is used
+ c.Check(snapInfo.Name(), Equals, "sample")
+ // Data from SideInfo is used
+ c.Check(snapInfo.Revision, Equals, snap.R(42))
+ // The YAML is placed on disk
+ cont, err := ioutil.ReadFile(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"))
+ c.Assert(err, IsNil)
+
+ c.Check(string(cont), Equals, sampleYaml)
+
+ // More
+ c.Check(snapInfo.Apps["app"].Command, Equals, "foo")
+ c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
+}
+
+func (s *snapTestSuite) TestMockInfo(c *C) {
+ snapInfo := snaptest.MockInfo(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
+ // Data from YAML is used
+ c.Check(snapInfo.Name(), Equals, "sample")
+ // Data from SideInfo is used
+ c.Check(snapInfo.Revision, Equals, snap.R(42))
+ // The YAML is *not* placed on disk
+ _, err := os.Stat(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"))
+ c.Assert(os.IsNotExist(err), Equals, true)
+ // More
+ c.Check(snapInfo.Apps["app"].Command, Equals, "foo")
+ c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package squashfs
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+// Magic is the magic prefix of squashfs snap files.
+var Magic = []byte{'h', 's', 'q', 's'}
+
+// Snap is the squashfs based snap.
+type Snap struct {
+ path string
+}
+
+// Path returns the path of the backing file.
+func (s *Snap) Path() string {
+ return s.path
+}
+
+// New returns a new Squashfs snap.
+func New(snapPath string) *Snap {
+ return &Snap{path: snapPath}
+}
+
+func (s *Snap) Install(targetPath, mountDir string) error {
+
+ // ensure mount-point and blob target dir.
+ for _, dir := range []string{mountDir, filepath.Dir(targetPath)} {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ }
+
+ // This is required so that the tests can simulate a mounted
+ // snap when we "install" a squashfs snap in the tests.
+ // We can not mount it for real in the tests, so we just unpack
+ // it to the location which is good enough for the tests.
+ if osutil.GetenvBool("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") {
+ if err := s.Unpack("*", mountDir); err != nil {
+ return err
+ }
+ }
+
+ // nothing to do, happens on e.g. first-boot when we already
+ // booted with the OS snap but its also in the seed.yaml
+ if s.path == targetPath || osutil.FilesAreEqual(s.path, targetPath) {
+ return nil
+ }
+
+ // try to (hard)link the file, but go on to trying to copy it
+ // if it fails for whatever reason
+ //
+ // link(2) returns EPERM on filesystems that don't support
+ // hard links (like vfat), so checking the error here doesn't
+ // make sense vs just trying to copy it.
+ if err := os.Link(s.path, targetPath); err == nil {
+ return nil
+ }
+
+ return osutil.CopyFile(s.path, targetPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
+}
+
+var runCommandWithOutput = func(args ...string) ([]byte, error) {
+ cmd := exec.Command(args[0], args[1:]...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return nil, fmt.Errorf("cmd: %q failed: %v (%q)", strings.Join(args, " "), err, output)
+ }
+
+ return output, nil
+}
+
+var runCommand = func(args ...string) error {
+ _, err := runCommandWithOutput(args...)
+ return err
+}
+
+func (s *Snap) Unpack(src, dstDir string) error {
+ return runCommand("unsquashfs", "-f", "-i", "-d", dstDir, s.path, src)
+}
+
+// Size returns the size of a squashfs snap.
+func (s *Snap) Size() (size int64, err error) {
+ st, err := os.Stat(s.path)
+ if err != nil {
+ return 0, err
+ }
+
+ return st.Size(), nil
+}
+
+// ReadFile returns the content of a single file inside a squashfs snap.
+func (s *Snap) ReadFile(filePath string) (content []byte, err error) {
+ tmpdir, err := ioutil.TempDir("", "read-file")
+ if err != nil {
+ return nil, err
+ }
+ defer os.RemoveAll(tmpdir)
+
+ unpackDir := filepath.Join(tmpdir, "unpack")
+ if err := runCommand("unsquashfs", "-i", "-d", unpackDir, s.path, filePath); err != nil {
+ return nil, err
+ }
+
+ return ioutil.ReadFile(filepath.Join(unpackDir, filePath))
+}
+
+// ListDir returns the content of a single directory inside a squashfs snap.
+func (s *Snap) ListDir(dirPath string) ([]string, error) {
+ output, err := runCommandWithOutput(
+ "unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath)
+ if err != nil {
+ return nil, err
+ }
+
+ prefixPath := path.Join("_", dirPath)
+ pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$")
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err)
+ }
+
+ var directoryContents []string
+ for _, groups := range pattern.FindAllSubmatch(output, -1) {
+ if len(groups) > 1 {
+ directoryContents = append(directoryContents, string(groups[1]))
+ }
+ }
+
+ return directoryContents, nil
+}
+
+// Build builds the snap.
+func (s *Snap) Build(buildDir string) error {
+ fullSnapPath, err := filepath.Abs(s.path)
+ if err != nil {
+ return err
+ }
+
+ return osutil.ChDir(buildDir, func() error {
+ return runCommand(
+ "mksquashfs",
+ ".", fullSnapPath,
+ "-noappend",
+ "-comp", "xz",
+ "-no-xattrs",
+ )
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package squashfs
+
+import (
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type SquashfsTestSuite struct {
+}
+
+var _ = Suite(&SquashfsTestSuite{})
+
+func makeSnap(c *C, manifest, data string) *Snap {
+ tmp := c.MkDir()
+ err := os.MkdirAll(filepath.Join(tmp, "meta", "hooks", "dir"), 0755)
+ c.Assert(err, IsNil)
+
+ // our regular snap.yaml
+ err = ioutil.WriteFile(filepath.Join(tmp, "meta", "snap.yaml"), []byte(manifest), 0644)
+ c.Assert(err, IsNil)
+
+ // some hooks
+ err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "foo-hook"), nil, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "bar-hook"), nil, 0755)
+ c.Assert(err, IsNil)
+ // And a file in another directory in there, just for testing (not a valid
+ // hook)
+ err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "dir", "baz"), nil, 0755)
+ c.Assert(err, IsNil)
+
+ // some data
+ err = ioutil.WriteFile(filepath.Join(tmp, "data.bin"), []byte(data), 0644)
+ c.Assert(err, IsNil)
+
+ // build it
+ cur, _ := os.Getwd()
+ snap := New(filepath.Join(cur, "foo.snap"))
+ err = snap.Build(tmp)
+ c.Assert(err, IsNil)
+
+ return snap
+}
+
+func (s *SquashfsTestSuite) SetUpTest(c *C) {
+ err := os.Chdir(c.MkDir())
+ c.Assert(err, IsNil)
+}
+
+func (s *SquashfsTestSuite) TestInstallSimple(c *C) {
+ snap := makeSnap(c, "name: test", "")
+ targetPath := filepath.Join(c.MkDir(), "target.snap")
+ mountDir := c.MkDir()
+ err := snap.Install(targetPath, mountDir)
+ c.Assert(err, IsNil)
+ c.Check(osutil.FileExists(targetPath), Equals, true)
+}
+
+func (s *SquashfsTestSuite) TestInstallNotCopyTwice(c *C) {
+ snap := makeSnap(c, "name: test2", "")
+ targetPath := filepath.Join(c.MkDir(), "target.snap")
+ mountDir := c.MkDir()
+ err := snap.Install(targetPath, mountDir)
+ c.Assert(err, IsNil)
+
+ cmd := testutil.MockCommand(c, "cp", "")
+ defer cmd.Restore()
+ err = snap.Install(targetPath, mountDir)
+ c.Assert(err, IsNil)
+ c.Assert(cmd.Calls(), HasLen, 0)
+}
+
+func (s *SquashfsTestSuite) TestPath(c *C) {
+ p := "/path/to/foo.snap"
+ snap := New("/path/to/foo.snap")
+ c.Assert(snap.Path(), Equals, p)
+}
+
+func (s *SquashfsTestSuite) TestReadFile(c *C) {
+ snap := makeSnap(c, "name: foo", "")
+
+ content, err := snap.ReadFile("meta/snap.yaml")
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "name: foo")
+}
+
+func (s *SquashfsTestSuite) TestListDir(c *C) {
+ snap := makeSnap(c, "name: foo", "")
+
+ fileNames, err := snap.ListDir("meta/hooks")
+ c.Assert(err, IsNil)
+ c.Assert(len(fileNames), Equals, 3)
+ c.Check(fileNames[0], Equals, "bar-hook")
+ c.Check(fileNames[1], Equals, "dir")
+ c.Check(fileNames[2], Equals, "foo-hook")
+}
+
+// TestUnpackGlob tests the internal unpack
+func (s *SquashfsTestSuite) TestUnpackGlob(c *C) {
+ data := "some random data"
+ snap := makeSnap(c, "", data)
+
+ outputDir := c.MkDir()
+ err := snap.Unpack("data*", outputDir)
+ c.Assert(err, IsNil)
+
+ // this is the file we expect
+ content, err := ioutil.ReadFile(filepath.Join(outputDir, "data.bin"))
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, data)
+
+ // ensure glob was honored
+ c.Assert(osutil.FileExists(filepath.Join(outputDir, "meta/snap.yaml")), Equals, false)
+}
+
+func (s *SquashfsTestSuite) TestBuild(c *C) {
+ buildDir := c.MkDir()
+ err := os.MkdirAll(filepath.Join(buildDir, "/random/dir"), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(buildDir, "data.bin"), []byte("data"), 0644)
+ c.Assert(err, IsNil)
+
+ snap := New(filepath.Join(c.MkDir(), "foo.snap"))
+ err = snap.Build(buildDir)
+ c.Assert(err, IsNil)
+
+ // unsquashfs writes a funny header like:
+ // "Parallel unsquashfs: Using 1 processor"
+ // "1 inodes (1 blocks) to write"
+ outputWithHeader, err := exec.Command("unsquashfs", "-n", "-l", snap.path).Output()
+ c.Assert(err, IsNil)
+ split := strings.Split(string(outputWithHeader), "\n")
+ output := strings.Join(split[3:], "\n")
+ c.Assert(string(output), Equals, `squashfs-root
+squashfs-root/data.bin
+squashfs-root/random
+squashfs-root/random/dir
+`)
+}
+
+func (s *SquashfsTestSuite) TestRunCommandGood(c *C) {
+ err := runCommand("true")
+ c.Assert(err, IsNil)
+}
+
+func (s *SquashfsTestSuite) TestRunCommandBad(c *C) {
+ err := runCommand("false")
+ c.Assert(err, ErrorMatches, regexp.QuoteMeta(`cmd: "false" failed: exit status 1 ("")`))
+}
+
+func (s *SquashfsTestSuite) TestRunCommandUgly(c *C) {
+ err := runCommand("cat", "/no/such/file")
+ c.Assert(err, ErrorMatches, regexp.QuoteMeta(`cmd: "cat /no/such/file" failed: exit status 1 ("cat: /no/such/file: No such file or directory\n")`))
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// Type represents the kind of snap (app, core, gadget, os, kernel)
+type Type string
+
+// The various types of snap parts we support
+const (
+ TypeApp Type = "app"
+ TypeGadget Type = "gadget"
+ TypeOS Type = "os"
+ TypeKernel Type = "kernel"
+)
+
+// UnmarshalJSON sets *m to a copy of data.
+func (m *Type) UnmarshalJSON(data []byte) error {
+ var str string
+ if err := json.Unmarshal(data, &str); err != nil {
+ return err
+ }
+
+ return m.fromString(str)
+}
+
+// UnmarshalYAML so ConfinementType implements yaml's Unmarshaler interface
+func (m *Type) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var str string
+ if err := unmarshal(&str); err != nil {
+ return err
+ }
+
+ return m.fromString(str)
+}
+
+// fromString converts str to Type and sets *m to it if validations pass
+func (m *Type) fromString(str string) error {
+ t := Type(str)
+
+ // this is a workaround as the store sends "application" but snappy uses
+ // "app" for TypeApp
+ if str == "application" {
+ t = TypeApp
+ }
+
+ if t != TypeApp && t != TypeGadget && t != TypeOS && t != TypeKernel {
+ return fmt.Errorf("invalid snap type: %q", str)
+ }
+
+ *m = t
+
+ return nil
+}
+
+// ConfinementType represents the kind of confinement supported by the snap
+// (devmode only, or strict confinement)
+type ConfinementType string
+
+// The various confinement types we support
+const (
+ DevModeConfinement ConfinementType = "devmode"
+ ClassicConfinement ConfinementType = "classic"
+ StrictConfinement ConfinementType = "strict"
+)
+
+// UnmarshalJSON sets *confinementType to a copy of data, assuming validation passes
+func (confinementType *ConfinementType) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+
+ return confinementType.fromString(s)
+}
+
+// UnmarshalYAML so ConfinementType implements yaml's Unmarshaler interface
+func (confinementType *ConfinementType) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var s string
+ if err := unmarshal(&s); err != nil {
+ return err
+ }
+
+ return confinementType.fromString(s)
+}
+
+func (confinementType *ConfinementType) fromString(str string) error {
+ c := ConfinementType(str)
+ if c != DevModeConfinement && c != ClassicConfinement && c != StrictConfinement {
+ return fmt.Errorf("invalid confinement type: %q", str)
+ }
+
+ *confinementType = c
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "encoding/json"
+ "fmt"
+ "gopkg.in/yaml.v2"
+
+ . "gopkg.in/check.v1"
+)
+
+type typeSuite struct{}
+
+var _ = Suite(&typeSuite{})
+
+func (s *typeSuite) TestJSONerr(c *C) {
+ var t Type
+ err := json.Unmarshal([]byte("false"), &t)
+ c.Assert(err, NotNil)
+}
+
+func (s *typeSuite) TestJsonMarshalTypes(c *C) {
+ out, err := json.Marshal(TypeApp)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "\"app\"")
+
+ out, err = json.Marshal(TypeGadget)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "\"gadget\"")
+
+ out, err = json.Marshal(TypeOS)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "\"os\"")
+
+ out, err = json.Marshal(TypeKernel)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "\"kernel\"")
+}
+
+func (s *typeSuite) TestJsonUnmarshalTypes(c *C) {
+ var st Type
+
+ err := json.Unmarshal([]byte("\"application\""), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeApp)
+
+ err = json.Unmarshal([]byte("\"app\""), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeApp)
+
+ err = json.Unmarshal([]byte("\"gadget\""), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeGadget)
+
+ err = json.Unmarshal([]byte("\"os\""), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeOS)
+
+ err = json.Unmarshal([]byte("\"kernel\""), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeKernel)
+}
+
+func (s *typeSuite) TestJsonUnmarshalInvalidTypes(c *C) {
+ invalidTypes := []string{"foo", "-app", "gadget_"}
+ var st Type
+ for _, invalidType := range invalidTypes {
+ err := json.Unmarshal([]byte(fmt.Sprintf("%q", invalidType)), &st)
+ c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid type", invalidType))
+ }
+}
+
+func (s *typeSuite) TestYamlMarshalTypes(c *C) {
+ out, err := yaml.Marshal(TypeApp)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "app\n")
+
+ out, err = yaml.Marshal(TypeGadget)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "gadget\n")
+
+ out, err = yaml.Marshal(TypeOS)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "os\n")
+
+ out, err = yaml.Marshal(TypeKernel)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "kernel\n")
+}
+
+func (s *typeSuite) TestYamlUnmarshalTypes(c *C) {
+ var st Type
+
+ err := yaml.Unmarshal([]byte("application"), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeApp)
+
+ err = yaml.Unmarshal([]byte("app"), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeApp)
+
+ err = yaml.Unmarshal([]byte("gadget"), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeGadget)
+
+ err = yaml.Unmarshal([]byte("os"), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeOS)
+
+ err = yaml.Unmarshal([]byte("kernel"), &st)
+ c.Assert(err, IsNil)
+ c.Check(st, Equals, TypeKernel)
+}
+
+func (s *typeSuite) TestYamlUnmarshalInvalidTypes(c *C) {
+ invalidTypes := []string{"foo", "-app", "gadget_"}
+ var st Type
+ for _, invalidType := range invalidTypes {
+ err := yaml.Unmarshal([]byte(invalidType), &st)
+ c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid type", invalidType))
+ }
+}
+
+func (s *typeSuite) TestYamlMarshalConfinementTypes(c *C) {
+ out, err := yaml.Marshal(DevModeConfinement)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "devmode\n")
+
+ out, err = yaml.Marshal(StrictConfinement)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "strict\n")
+}
+
+func (s *typeSuite) TestYamlUnmarshalConfinementTypes(c *C) {
+ var confinementType ConfinementType
+ err := yaml.Unmarshal([]byte("devmode"), &confinementType)
+ c.Assert(err, IsNil)
+ c.Check(confinementType, Equals, DevModeConfinement)
+
+ err = yaml.Unmarshal([]byte("strict"), &confinementType)
+ c.Assert(err, IsNil)
+ c.Check(confinementType, Equals, StrictConfinement)
+}
+
+func (s *typeSuite) TestYamlUnmarshalInvalidConfinementTypes(c *C) {
+ var invalidConfinementTypes = []string{
+ "foo", "strict-", "_devmode",
+ }
+ var confinementType ConfinementType
+ for _, thisConfinementType := range invalidConfinementTypes {
+ err := yaml.Unmarshal([]byte(thisConfinementType), &confinementType)
+ c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid confinement type", thisConfinementType))
+ }
+}
+
+func (s *typeSuite) TestJsonMarshalConfinementTypes(c *C) {
+ out, err := json.Marshal(DevModeConfinement)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "\"devmode\"")
+
+ out, err = json.Marshal(StrictConfinement)
+ c.Assert(err, IsNil)
+ c.Check(string(out), Equals, "\"strict\"")
+}
+
+func (s *typeSuite) TestJsonUnmarshalConfinementTypes(c *C) {
+ var confinementType ConfinementType
+ err := json.Unmarshal([]byte("\"devmode\""), &confinementType)
+ c.Assert(err, IsNil)
+ c.Check(confinementType, Equals, DevModeConfinement)
+
+ err = json.Unmarshal([]byte("\"strict\""), &confinementType)
+ c.Assert(err, IsNil)
+ c.Check(confinementType, Equals, StrictConfinement)
+}
+
+func (s *typeSuite) TestJsonUnmarshalInvalidConfinementTypes(c *C) {
+ var invalidConfinementTypes = []string{
+ "foo", "strict-", "_devmode",
+ }
+ var confinementType ConfinementType
+ for _, thisConfinementType := range invalidConfinementTypes {
+ err := json.Unmarshal([]byte(fmt.Sprintf("%q", thisConfinementType)), &confinementType)
+ c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid confinement type", thisConfinementType))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap
+
+import (
+ "fmt"
+ "regexp"
+)
+
+// Regular expression describing correct identifiers.
+var validSnapName = regexp.MustCompile("^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$")
+var validEpoch = regexp.MustCompile("^(?:0|[1-9][0-9]*[*]?)$")
+var validHookName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$")
+
+// ValidateName checks if a string can be used as a snap name.
+func ValidateName(name string) error {
+ valid := validSnapName.MatchString(name)
+ if !valid {
+ return fmt.Errorf("invalid snap name: %q", name)
+ }
+ return nil
+}
+
+// ValidateEpoch checks if a string can be used as a snap epoch.
+func ValidateEpoch(epoch string) error {
+ valid := validEpoch.MatchString(epoch)
+ if !valid {
+ return fmt.Errorf("invalid snap epoch: %q", epoch)
+ }
+ return nil
+}
+
+// ValidateHook validates the content of the given HookInfo
+func ValidateHook(hook *HookInfo) error {
+ valid := validHookName.MatchString(hook.Name)
+ if !valid {
+ return fmt.Errorf("invalid hook name: %q", hook.Name)
+ }
+ return nil
+}
+
+var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$")
+
+// Validate verifies the content in the info.
+func Validate(info *Info) error {
+ name := info.Name()
+ if name == "" {
+ return fmt.Errorf("snap name cannot be empty")
+ }
+ err := ValidateName(name)
+ if err != nil {
+ return err
+ }
+
+ epoch := info.Epoch
+ if epoch == "" {
+ return fmt.Errorf("snap epoch cannot be empty")
+ }
+ err = ValidateEpoch(epoch)
+ if err != nil {
+ return err
+ }
+
+ // validate app entries
+ for _, app := range info.Apps {
+ err := ValidateApp(app)
+ if err != nil {
+ return err
+ }
+ }
+
+ // validate aliases
+ for alias, app := range info.Aliases {
+ if !validAlias.MatchString(alias) {
+ return fmt.Errorf("cannot have %q as alias name for app %q - use only letters, digits, dash, underscore and dot characters", alias, app.Name)
+ }
+ }
+
+ // validate hook entries
+ for _, hook := range info.Hooks {
+ err := ValidateHook(hook)
+ if err != nil {
+ return err
+ }
+ }
+
+ // ensure that plug and slot have unique names
+ if err := plugsSlotsUniqueNames(info); err != nil {
+ return err
+ }
+ return nil
+}
+
+func plugsSlotsUniqueNames(info *Info) error {
+ // we could choose the smaller collection if we wanted to optimize this check
+ for plugName := range info.Plugs {
+ if info.Slots[plugName] != nil {
+ return fmt.Errorf("cannot have plug and slot with the same name: %q", plugName)
+ }
+ }
+ return nil
+}
+
+func validateField(name, cont string, whitelist *regexp.Regexp) error {
+ if !whitelist.MatchString(cont) {
+ return fmt.Errorf("app description field '%s' contains illegal %q (legal: '%s')", name, cont, whitelist)
+
+ }
+ return nil
+}
+
+// appContentWhitelist is the whitelist of legal chars in the "apps"
+// section of snap.yaml
+var appContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/. _#:-]*$`)
+var validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$")
+
+// ValidateApp verifies the content in the app info.
+func ValidateApp(app *AppInfo) error {
+ switch app.Daemon {
+ case "", "simple", "forking", "oneshot", "dbus", "notify":
+ // valid
+ default:
+ return fmt.Errorf(`"daemon" field contains invalid value %q`, app.Daemon)
+ }
+
+ // Validate app name
+ if !validAppName.MatchString(app.Name) {
+ return fmt.Errorf("cannot have %q as app name - use letters, digits, and dash as separator", app.Name)
+ }
+
+ // Validate the rest of the app info
+ checks := map[string]string{
+ "command": app.Command,
+ "stop-command": app.StopCommand,
+ "post-stop-command": app.PostStopCommand,
+ "bus-name": app.BusName,
+ }
+
+ for name, value := range checks {
+ if err := validateField(name, value, appContentWhitelist); err != nil {
+ return err
+ }
+ }
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package snap_test
+
+import (
+ "fmt"
+ "regexp"
+
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/snap"
+)
+
+type ValidateSuite struct{}
+
+var _ = Suite(&ValidateSuite{})
+
+func (s *ValidateSuite) TestValidateName(c *C) {
+ validNames := []string{
+ "a", "aa", "aaa", "aaaa",
+ "a-a", "aa-a", "a-aa", "a-b-c",
+ "a0", "a-0", "a-0a",
+ "01game", "1-or-2",
+ }
+ for _, name := range validNames {
+ err := ValidateName(name)
+ c.Assert(err, IsNil)
+ }
+ invalidNames := []string{
+ // name cannot be empty
+ "",
+ // dashes alone are not a name
+ "-", "--",
+ // double dashes in a name are not allowed
+ "a--a",
+ // name should not end with a dash
+ "a-",
+ // name cannot have any spaces in it
+ "a ", " a", "a a",
+ // a number alone is not a name
+ "0", "123",
+ // identifier must be plain ASCII
+ "日本語", "한글", "ру́сский язы́к",
+ }
+ for _, name := range invalidNames {
+ err := ValidateName(name)
+ c.Assert(err, ErrorMatches, `invalid snap name: ".*"`)
+ }
+}
+
+func (s *ValidateSuite) TestValidateEpoch(c *C) {
+ validEpochs := []string{
+ "0", "1*", "1", "400*", "1234",
+ }
+ for _, epoch := range validEpochs {
+ err := ValidateEpoch(epoch)
+ c.Assert(err, IsNil)
+ }
+ invalidEpochs := []string{
+ "0*", "_", "1-", "1+", "-1", "+1", "-1*", "a", "1a", "1**",
+ }
+ for _, epoch := range invalidEpochs {
+ err := ValidateEpoch(epoch)
+ c.Assert(err, ErrorMatches, `invalid snap epoch: ".*"`)
+ }
+}
+
+func (s *ValidateSuite) TestValidateHook(c *C) {
+ validHooks := []*HookInfo{
+ {Name: "a"},
+ {Name: "aaa"},
+ {Name: "a-a"},
+ {Name: "aa-a"},
+ {Name: "a-aa"},
+ {Name: "a-b-c"},
+ }
+ for _, hook := range validHooks {
+ err := ValidateHook(hook)
+ c.Assert(err, IsNil)
+ }
+ invalidHooks := []*HookInfo{
+ {Name: ""},
+ {Name: "a a"},
+ {Name: "a--a"},
+ {Name: "-a"},
+ {Name: "a-"},
+ {Name: "0"},
+ {Name: "123"},
+ {Name: "123abc"},
+ {Name: "日本語"},
+ }
+ for _, hook := range invalidHooks {
+ err := ValidateHook(hook)
+ c.Assert(err, ErrorMatches, `invalid hook name: ".*"`)
+ }
+}
+
+// ValidateApp
+
+func (s *ValidateSuite) TestValidateAppName(c *C) {
+ validAppNames := []string{
+ "1", "a", "aa", "aaa", "aaaa", "Aa", "aA", "1a", "a1", "1-a", "a-1",
+ "a-a", "aa-a", "a-aa", "a-b-c", "0a-a", "a-0a",
+ }
+ for _, name := range validAppNames {
+ c.Check(ValidateApp(&AppInfo{Name: name}), IsNil)
+ }
+ invalidAppNames := []string{
+ "", "-", "--", "a--a", "a-", "a ", " a", "a a", "日本語", "한글",
+ "ру́сский язы́к", "ໄຂ່ອີສເຕີ້", ":a", "a:", "a:a", "_a", "a_", "a_a",
+ }
+ for _, name := range invalidAppNames {
+ err := ValidateApp(&AppInfo{Name: name})
+ c.Assert(err, ErrorMatches, `cannot have ".*" as app name.*`)
+ }
+}
+
+func (s *ValidateSuite) TestAppWhitelistSimple(c *C) {
+ c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo"}), IsNil)
+ c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo"}), IsNil)
+ c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo"}), IsNil)
+}
+
+func (s *ValidateSuite) TestAppWhitelistIllegal(c *C) {
+ c.Check(ValidateApp(&AppInfo{Name: "x\n"}), NotNil)
+ c.Check(ValidateApp(&AppInfo{Name: "test!me"}), NotNil)
+ c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo\n"}), NotNil)
+ c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo\n"}), NotNil)
+ c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo\n"}), NotNil)
+ c.Check(ValidateApp(&AppInfo{Name: "foo", BusName: "foo\n"}), NotNil)
+}
+
+func (s *ValidateSuite) TestAppDaemonValue(c *C) {
+ for _, t := range []struct {
+ daemon string
+ ok bool
+ }{
+ // good
+ {"", true},
+ {"simple", true},
+ {"forking", true},
+ {"oneshot", true},
+ {"dbus", true},
+ {"notify", true},
+ // bad
+ {"invalid-thing", false},
+ } {
+ if t.ok {
+ c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: t.daemon}), IsNil)
+ } else {
+ c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: t.daemon}), ErrorMatches, fmt.Sprintf(`"daemon" field contains invalid value %q`, t.daemon))
+ }
+ }
+}
+
+func (s *ValidateSuite) TestAppWhitelistError(c *C) {
+ err := ValidateApp(&AppInfo{Name: "foo", Command: "x\n"})
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, `app description field 'command' contains illegal "x\n" (legal: '^[A-Za-z0-9/. _#:-]*$')`)
+}
+
+// Validate
+
+func (s *ValidateSuite) TestDetectIllegalYamlBinaries(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: foo
+version: 1.0
+apps:
+ tes!me:
+ command: someething
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, NotNil)
+}
+
+func (s *ValidateSuite) TestDetectIllegalYamlService(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: foo
+version: 1.0
+apps:
+ tes!me:
+ command: something
+ daemon: forking
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, NotNil)
+}
+
+func (s *ValidateSuite) TestIllegalSnapName(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: foo.something
+version: 1.0
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, ErrorMatches, `invalid snap name: "foo.something"`)
+}
+
+func (s *ValidateSuite) TestValidateChecksName(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`
+version: 1.0
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, ErrorMatches, `snap name cannot be empty`)
+}
+
+func (s *ValidateSuite) TestIllegalSnapEpoch(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: foo
+version: 1.0
+epoch: 0*
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, ErrorMatches, `invalid snap epoch: "0\*"`)
+}
+
+func (s *ValidateSuite) TestMissingSnapEpochIsOkay(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: foo
+version: 1.0
+`))
+ c.Assert(err, IsNil)
+ c.Assert(Validate(info), IsNil)
+}
+
+func (s *ValidateSuite) TestIllegalHookName(c *C) {
+ hookType := NewHookType(regexp.MustCompile(".*"))
+ restore := MockSupportedHookTypes([]*HookType{hookType})
+ defer restore()
+
+ info, err := InfoFromSnapYaml([]byte(`name: foo
+version: 1.0
+hooks:
+ 123abc:
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, ErrorMatches, `invalid hook name: "123abc"`)
+}
+
+func (s *ValidateSuite) TestPlugSlotNamesUnique(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: snap
+plugs:
+ foo:
+slots:
+ foo:
+`))
+ c.Assert(err, IsNil)
+ err = Validate(info)
+ c.Check(err, ErrorMatches, `cannot have plug and slot with the same name: "foo"`)
+}
+
+func (s *ValidateSuite) TestIllegalAliasName(c *C) {
+ info, err := InfoFromSnapYaml([]byte(`name: foo
+version: 1.0
+apps:
+ foo:
+ aliases: [foo$]
+`))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ c.Check(err, ErrorMatches, `cannot have "foo\$" as alias name for app "foo" - use only letters, digits, dash, underscore and dot characters`)
+}
--- /dev/null
+project: snapd
+
+environment:
+ GOPATH: /home/gopath
+ REUSE_PROJECT: "$(HOST: echo $REUSE_PROJECT)"
+ PROJECT_PATH: $GOPATH/src/github.com/snapcore/snapd
+ # /usr/lib/go-1.6/bin for trusty (needs to be last as we use
+ # a different go in gccgo tests)
+ PATH: $GOPATH/bin:/snap/bin:$PATH:/usr/lib/go-1.6/bin
+ TESTSLIB: $PROJECT_PATH/tests/lib
+ SNAPPY_TESTING: 1
+ # we run the entire suite with re-exec on (the default) and modify
+ # the core snap so that it contains our new code. So we run new
+ # snapd from the deb that re-execs into new snapd in core. To
+ # test purely from the deb, set "export SPREAD_SNAP_REEXEC=0"
+ SNAP_REEXEC: "$(HOST: echo ${SPREAD_SNAP_REEXEC:-})"
+ MODIFY_CORE_SNAP_FOR_REEXEC: "$(HOST: echo ${SPREAD_MODIFY_CORE_SNAP_FOR_REEXEC:-1})"
+ SPREAD_STORE_USER: "$(HOST: echo $SPREAD_STORE_USER)"
+ SPREAD_STORE_PASSWORD: "$(HOST: echo $SPREAD_STORE_PASSWORD)"
+ LANG: "$(echo ${LANG:-C.UTF-8})"
+ LANGUAGE: "$(echo ${LANGUAGE:-en})"
+ # important to ensure adhoc and linode/qemu behave the same
+ SUDO_USER: ""
+ SUDO_UID: ""
+ TRUST_TEST_KEYS: "true"
+ MANAGED_DEVICE: "false"
+ CORE_CHANNEL: "$(HOST: echo ${SPREAD_CORE_CHANNEL:-edge})"
+ # slight abuse
+ GENERATE_14_04: "$(HOST: while [ `pwd` != / ] && [ ! -f spread.yaml ]; do cd ..; done && ./generate-packaging-dir ubuntu 14.04)"
+
+backends:
+ linode:
+ key: "$(HOST: echo $SPREAD_LINODE_KEY)"
+ halt-timeout: 2h
+ systems:
+ - ubuntu-14.04-64:
+ kernel: GRUB 2
+ - ubuntu-16.04-64:
+ kernel: GRUB 2
+ workers: 2
+ - ubuntu-16.04-32:
+ kernel: GRUB 2
+ workers: 2
+ - ubuntu-core-16-64:
+ kernel: Direct Disk
+ image: ubuntu-16.04-64
+ # FIXME restore of ubuntu-core does not properly reset
+ # boot variables and key snaps to their pristine state.
+ - ubuntu-core-16-64-fixme:
+ kernel: Direct Disk
+ image: ubuntu-16.04-64
+ qemu:
+ environment:
+ APT_PROXY: "$(HOST: echo $APT_PROXY)"
+ systems:
+ - ubuntu-14.04-32:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-14.04-64:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.04-32:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.04-64:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-core-16-64:
+ image: ubuntu-16.04-64
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.10-64:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-17.04-64:
+ username: ubuntu
+ password: ubuntu
+ autopkgtest:
+ type: adhoc
+ allocate: |
+ echo "Allocating ad-hoc $SPREAD_SYSTEM"
+ if [ -z "${ADT_ARTIFACTS}" ]; then
+ FATAL "adhoc only works inside autopkgtest"
+ exit 1
+ fi
+ echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/99-spread-users
+ ADDRESS localhost:22
+ discard: |
+ echo "Discarding ad-hoc $SPREAD_SYSTEM"
+ systems:
+ - ubuntu-16.04-amd64:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.04-i386:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.04-ppc64el:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.04-armhf:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.10-amd64:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.10-i386:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.10-ppc64el:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-16.10-armhf:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-17.04-amd64:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-17.04-i386:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-17.04-ppc64el:
+ username: ubuntu
+ password: ubuntu
+ - ubuntu-17.04-armhf:
+ username: ubuntu
+ password: ubuntu
+ external:
+ type: adhoc
+ environment:
+ SPREAD_EXTERNAL_ADDRESS: "$(HOST: echo ${SPREAD_EXTERNAL_ADDRESS:-localhost:8022})"
+ MANAGED_DEVICE: "true"
+ allocate: |
+ ADDRESS $SPREAD_EXTERNAL_ADDRESS
+ systems:
+ - ubuntu-core-16-64:
+ environment:
+ TRUST_TEST_KEYS: "false"
+ username: test
+ password: ubuntu
+ - ubuntu-core-16-arm-64:
+ username: test
+ password: ubuntu
+ - ubuntu-core-16-arm-32:
+ username: test
+ password: ubuntu
+
+path: /home/gopath/src/github.com/snapcore/snapd
+
+exclude:
+ - .git
+
+prepare-each: |
+ # systemd on 14.04 does not know about --rotate
+ # or --vacuum-time.
+ # TODO: Find a way to clean out systemd logs on
+ # systemd 204.
+ if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then
+ journalctl --rotate
+ sleep .1
+ journalctl --vacuum-time=1ms
+ fi
+ dmesg -c > /dev/null
+
+debug-each: |
+ journalctl -u snapd
+ dmesg | grep DENIED || true
+
+prepare: |
+ # this indicates that the server got reused, nothing to setup
+ [ "$REUSE_PROJECT" != 1 ] || exit 0
+ echo "Running with SNAP_REEXEC: $SNAP_REEXEC"
+ # check that we are not updating
+ . "$TESTSLIB/boot.sh"
+ if [ "$(bootenv snap_mode)" = "try" ]; then
+ echo "Ongoing reboot upgrade process, please try again when finished"
+ exit 1
+ fi
+
+ if [ "$SPREAD_BACKEND" = external ]; then
+ # build test binaries
+ if [ ! -f $GOPATH/bin/snapbuild ]; then
+ mkdir -p $GOPATH/bin
+ snap install --devmode --edge classic
+ classic "sudo apt update && apt install -y git golang-go build-essential"
+ classic "GOPATH=$GOPATH go get ../..${PROJECT_PATH}/tests/lib/snapbuild"
+ snap remove classic
+ fi
+ # stop and disable autorefresh
+ systemctl disable --now snapd.refresh.timer
+ exit 0
+ fi
+
+ if [ "$SPREAD_BACKEND" = qemu ]; then
+ # treat APT_PROXY as a location of apt-cacher-ng to use
+ if [ -d /etc/apt/apt.conf.d ] && [ -n "${APT_PROXY:-}" ]; then
+ printf 'Acquire::http::Proxy "%s";\n' "$APT_PROXY" > /etc/apt/apt.conf.d/99proxy
+ fi
+ fi
+
+ # apt update is hanging on security.ubuntu.com with IPv6.
+ sysctl -w net.ipv6.conf.all.disable_ipv6=1
+ trap "sysctl -w net.ipv6.conf.all.disable_ipv6=0" EXIT
+
+ apt-get update
+
+ # XXX: This seems to be required. Otherwise package build
+ # fails with unmet dependency on "build-essential:native"
+ apt-get install -y build-essential
+
+ apt-get install -y software-properties-common
+
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ if [ ! -d debian-ubuntu-14.04 ]; then
+ echo "no debian-ubuntu-14.04/ directory "
+ echo "broken test setup"
+ exit 1
+ fi
+
+ # 14.04 has its own packaging
+ rm -rf debian
+ mv debian-ubuntu-14.04 debian
+
+ echo 'deb http://archive.ubuntu.com/ubuntu/ trusty-proposed main universe' >> /etc/apt/sources.list
+ apt-get update
+
+ add-apt-repository ppa:snappy-dev/image
+ apt-get update
+ apt-get install -y --install-recommends linux-generic-lts-xenial
+ apt-get install -y --force-yes apparmor libapparmor1 seccomp libseccomp2 systemd cgroup-lite util-linux
+
+ fi
+
+ apt-get purge -y snapd snap-confine ubuntu-core-launcher
+ apt-get update
+ # utilities
+ apt-get install -y curl devscripts expect gdebi-core jq rng-tools git
+
+ # in 16.04: apt build-dep -y ./
+ apt-get install -y $(gdebi --quiet --apt-line ./debian/control)
+
+ # update vendoring
+ if [ "$(which govendor)" = "" ]; then
+ rm -rf $GOPATH/src/github.com/kardianos/govendor
+ go get -u github.com/kardianos/govendor
+ fi
+ govendor sync
+
+ # increment version so upgrade can work, use "zzz" as version
+ # component to ensure that its higher than any "ubuntuN" version
+ # that might also be in the archive
+ dch -lzzz "testing build"
+
+ if ! id test >& /dev/null; then
+ # manually setting the UID and GID to 12345 because we need to
+ # know the numbers match for when we set up the user inside
+ # the all-snap, which has its own user & group database.
+ # Nothing special about 12345 beyond it being high enough it's
+ # unlikely to ever clash with anything, and easy to remember.
+ addgroup --quiet --gid 12345 test
+ adduser --quiet --uid 12345 --gid 12345 --disabled-password --gecos '' test
+ fi
+
+ owner=$( stat -c "%U:%G" /home/test )
+ if [ "$owner" != "test:test" ]; then
+ echo "expected /home/test to be test:test but it's $owner"
+ exit 1
+ fi
+ unset owner
+
+ echo 'test ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
+ chown test.test -R ..
+ su -l -c "cd $PWD && DEB_BUILD_OPTIONS='nocheck testkeys' dpkg-buildpackage -tc -b -Zgzip" test
+ # put our debs to a safe place
+ cp ../*.deb $GOPATH
+
+ # Build snapbuild.
+ apt-get install -y git
+ go get ./tests/lib/snapbuild
+
+ # Build fakestore.
+ go get ./tests/lib/fakestore/cmd/fakestore
+ # Build fakedevicesvc.
+ go get ./tests/lib/fakedevicesvc
+
+restore: |
+ if [ "$SPREAD_BACKEND" = external ]; then
+ # start and enable autorefresh
+ systemctl enable --now snapd.refresh.timer
+ fi
+
+suites:
+ tests/main/:
+ summary: Full-system tests for snapd
+ systems: [-ubuntu-core-16-64-fixme]
+ prepare: |
+ . $TESTSLIB/prepare.sh
+ if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then
+ prepare_all_snap
+ else
+ prepare_classic
+ fi
+ restore: |
+ $TESTSLIB/reset.sh
+ if [[ "$SPREAD_SYSTEM" != ubuntu-core-16-* ]]; then
+ apt-get purge -y snapd snap-confine ubuntu-core-launcher
+ fi
+ restore-each: |
+ $TESTSLIB/reset.sh --reuse-core
+
+ tests/regression/:
+ summary: Regression tests for snapd
+ systems: [-ubuntu-core-16-64-fixme]
+ prepare: |
+ . $TESTSLIB/prepare.sh
+ if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then
+ prepare_all_snap
+ else
+ prepare_classic
+ fi
+ restore: |
+ $TESTSLIB/reset.sh
+ if [[ "$SPREAD_SYSTEM" != ubuntu-core-16-* ]]; then
+ apt-get purge -y snapd snap-confine ubuntu-core-launcher
+ fi
+ restore-each: |
+ $TESTSLIB/reset.sh --reuse-core
+
+ tests/upgrade/:
+ summary: Tests for snapd upgrade
+ systems: [-ubuntu-core-16-64-fixme]
+ restore:
+ apt-get purge -y snapd snap-confine ubuntu-core-launcher
+ restore-each: |
+ $TESTSLIB/reset.sh
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+
+ "gopkg.in/macaroon.v1"
+)
+
+var (
+ myappsAPIBase = myappsURL()
+ // MyAppsMacaroonACLAPI points to MyApps endpoint to get a ACL macaroon
+ MyAppsMacaroonACLAPI = myappsAPIBase + "dev/api/acl/"
+ // MyAppsDeviceNonceAPI points to MyApps endpoint to get a nonce
+ MyAppsDeviceNonceAPI = myappsAPIBase + "identity/api/v1/nonces"
+ // MyAppsDeviceSessionAPI points to MyApps endpoint to get a device session
+ MyAppsDeviceSessionAPI = myappsAPIBase + "identity/api/v1/sessions"
+ ubuntuoneAPIBase = authURL()
+ // UbuntuoneLocation is the Ubuntuone location as defined in the store macaroon
+ UbuntuoneLocation = authLocation()
+ // UbuntuoneDischargeAPI points to SSO endpoint to discharge a macaroon
+ UbuntuoneDischargeAPI = ubuntuoneAPIBase + "/tokens/discharge"
+ // UbuntuoneRefreshDischargeAPI points to SSO endpoint to refresh a discharge macaroon
+ UbuntuoneRefreshDischargeAPI = ubuntuoneAPIBase + "/tokens/refresh"
+)
+
+type ssoMsg struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ Extra map[string][]string `json:"extra"`
+}
+
+// returns true if the http status code is in the "success" range (2xx)
+func httpStatusCodeSuccess(httpStatusCode int) bool {
+ return httpStatusCode/100 == 2
+}
+
+// returns true if the http status code is in the "client-error" range (4xx)
+func httpStatusCodeClientError(httpStatusCode int) bool {
+ return httpStatusCode/100 == 4
+}
+
+// loginCaveatID returns the 3rd party caveat from the macaroon to be discharged by Ubuntuone
+func loginCaveatID(m *macaroon.Macaroon) (string, error) {
+ caveatID := ""
+ for _, caveat := range m.Caveats() {
+ if caveat.Location == UbuntuoneLocation {
+ caveatID = caveat.Id
+ break
+ }
+ }
+ if caveatID == "" {
+ return "", fmt.Errorf("missing login caveat")
+ }
+ return caveatID, nil
+}
+
+// requestStoreMacaroon requests a macaroon for accessing package data from the ubuntu store.
+func requestStoreMacaroon() (string, error) {
+ const errorPrefix = "cannot get snap access permission from store: "
+
+ data := map[string]interface{}{
+ "permissions": []string{"package_access", "package_purchase"},
+ }
+ macaroonJSONData, err := json.Marshal(data)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ req, err := http.NewRequest("POST", MyAppsMacaroonACLAPI, bytes.NewReader(macaroonJSONData))
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ defer resp.Body.Close()
+
+ // check return code, error on anything !200
+ if resp.StatusCode != 200 {
+ return "", fmt.Errorf(errorPrefix+"store server returned status %d", resp.StatusCode)
+ }
+
+ dec := json.NewDecoder(resp.Body)
+ var responseData struct {
+ Macaroon string `json:"macaroon"`
+ }
+ if err := dec.Decode(&responseData); err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ if responseData.Macaroon == "" {
+ return "", fmt.Errorf(errorPrefix + "empty macaroon returned")
+ }
+ return responseData.Macaroon, nil
+}
+
+func requestDischargeMacaroon(endpoint string, data map[string]string) (string, error) {
+ const errorPrefix = "cannot authenticate to snap store: "
+
+ dischargeJSONData, err := json.Marshal(data)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ req, err := http.NewRequest("POST", endpoint, bytes.NewReader(dischargeJSONData))
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ defer resp.Body.Close()
+
+ // check return code, error on 4xx and anything !200
+ switch {
+ case httpStatusCodeClientError(resp.StatusCode):
+ // get error details
+ var msg ssoMsg
+ dec := json.NewDecoder(resp.Body)
+
+ if err := dec.Decode(&msg); err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ switch msg.Code {
+ case "TWOFACTOR_REQUIRED":
+ return "", ErrAuthenticationNeeds2fa
+ case "TWOFACTOR_FAILURE":
+ return "", Err2faFailed
+ case "INVALID_DATA":
+ return "", ErrInvalidAuthData(msg.Extra)
+ }
+
+ if msg.Message != "" {
+ return "", fmt.Errorf(errorPrefix+"%v", msg.Message)
+ }
+ fallthrough
+
+ case !httpStatusCodeSuccess(resp.StatusCode):
+ return "", fmt.Errorf(errorPrefix+"server returned status %d", resp.StatusCode)
+ }
+
+ dec := json.NewDecoder(resp.Body)
+ var responseData struct {
+ Macaroon string `json:"discharge_macaroon"`
+ }
+ if err := dec.Decode(&responseData); err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ if responseData.Macaroon == "" {
+ return "", fmt.Errorf(errorPrefix + "empty macaroon returned")
+ }
+ return responseData.Macaroon, nil
+}
+
+// dischargeAuthCaveat returns a macaroon with the store auth caveat discharged.
+func dischargeAuthCaveat(caveat, username, password, otp string) (string, error) {
+ data := map[string]string{
+ "email": username,
+ "password": password,
+ "caveat_id": caveat,
+ }
+ if otp != "" {
+ data["otp"] = otp
+ }
+
+ return requestDischargeMacaroon(UbuntuoneDischargeAPI, data)
+}
+
+// refreshDischargeMacaroon returns a soft-refreshed discharge macaroon.
+func refreshDischargeMacaroon(discharge string) (string, error) {
+ data := map[string]string{
+ "discharge_macaroon": discharge,
+ }
+
+ return requestDischargeMacaroon(UbuntuoneRefreshDischargeAPI, data)
+}
+
+// requestStoreDeviceNonce requests a nonce for device authentication against the store.
+func requestStoreDeviceNonce() (string, error) {
+ const errorPrefix = "cannot get nonce from store: "
+
+ req, err := http.NewRequest("POST", MyAppsDeviceNonceAPI, nil)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("Accept", "application/json")
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ defer resp.Body.Close()
+
+ // check return code, error on anything !200
+ if resp.StatusCode != 200 {
+ return "", fmt.Errorf(errorPrefix+"store server returned status %d", resp.StatusCode)
+ }
+
+ dec := json.NewDecoder(resp.Body)
+ var responseData struct {
+ Nonce string `json:"nonce"`
+ }
+ if err := dec.Decode(&responseData); err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ if responseData.Nonce == "" {
+ return "", fmt.Errorf(errorPrefix + "empty nonce returned")
+ }
+ return responseData.Nonce, nil
+}
+
+// requestDeviceSession requests a device session macaroon from the store.
+func requestDeviceSession(serialAssertion, sessionRequest, previousSession string) (string, error) {
+ const errorPrefix = "cannot get device session from store: "
+
+ data := map[string]string{
+ "serial-assertion": serialAssertion,
+ "device-session-request": sessionRequest,
+ }
+ deviceJSONData, err := json.Marshal(data)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ req, err := http.NewRequest("POST", MyAppsDeviceSessionAPI, bytes.NewReader(deviceJSONData))
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+ if previousSession != "" {
+ req.Header.Set("X-Device-Authorization", fmt.Sprintf(`Macaroon root="%s"`, previousSession))
+ }
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+ defer resp.Body.Close()
+
+ // check return code, error on anything !200
+ if resp.StatusCode != 200 {
+ body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1e6)) // do our best to read the body
+ return "", fmt.Errorf(errorPrefix+"store server returned status %d and body %q", resp.StatusCode, body)
+ }
+
+ dec := json.NewDecoder(resp.Body)
+ var responseData struct {
+ Macaroon string `json:"macaroon"`
+ }
+ if err := dec.Decode(&responseData); err != nil {
+ return "", fmt.Errorf(errorPrefix+"%v", err)
+ }
+
+ if responseData.Macaroon == "" {
+ return "", fmt.Errorf(errorPrefix + "empty session returned")
+ }
+ return responseData.Macaroon, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/macaroon.v1"
+)
+
+type authTestSuite struct{}
+
+var _ = Suite(&authTestSuite{})
+
+const mockStoreInvalidLoginCode = 401
+const mockStoreInvalidLogin = `
+{
+ "message": "Provided email/password is not correct.",
+ "code": "INVALID_CREDENTIALS",
+ "extra": {}
+}
+`
+
+const mockStoreNeeds2faHTTPCode = 401
+const mockStoreNeeds2fa = `
+{
+ "message": "2-factor authentication required.",
+ "code": "TWOFACTOR_REQUIRED",
+ "extra": {}
+}
+`
+
+const mockStore2faFailedHTTPCode = 403
+const mockStore2faFailedResponse = `
+{
+ "message": "The provided 2-factor key is not recognised.",
+ "code": "TWOFACTOR_FAILURE",
+ "extra": {}
+}
+`
+
+const mockStoreReturnMacaroon = `{"macaroon": "the-root-macaroon-serialized-data"}`
+
+const mockStoreReturnDischarge = `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}`
+
+const mockStoreReturnNoMacaroon = `{}`
+
+const mockStoreReturnNonce = `{"nonce": "the-nonce"}`
+
+const mockStoreReturnNoNonce = `{}`
+
+func (s *authTestSuite) TestRequestStoreMacaroon(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnMacaroon)
+ }))
+ defer mockServer.Close()
+ MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
+
+ macaroon, err := requestStoreMacaroon()
+ c.Assert(err, IsNil)
+ c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data")
+}
+
+func (s *authTestSuite) TestRequestStoreMacaroonMissingData(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnNoMacaroon)
+ }))
+ defer mockServer.Close()
+ MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
+
+ macaroon, err := requestStoreMacaroon()
+ c.Assert(err, ErrorMatches, "cannot get snap access permission from store: empty macaroon returned")
+ c.Assert(macaroon, Equals, "")
+}
+
+func (s *authTestSuite) TestRequestStoreMacaroonError(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(500)
+ }))
+ defer mockServer.Close()
+ MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
+
+ macaroon, err := requestStoreMacaroon()
+ c.Assert(err, ErrorMatches, "cannot get snap access permission from store: store server returned status 500")
+ c.Assert(macaroon, Equals, "")
+}
+
+func (s *authTestSuite) TestDischargeAuthCaveat(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnDischarge)
+ }))
+ defer mockServer.Close()
+ UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
+
+ discharge, err := dischargeAuthCaveat("third-party-caveat", "guy@example.com", "passwd", "")
+ c.Assert(err, IsNil)
+ c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data")
+}
+
+func (s *authTestSuite) TestDischargeAuthCaveatNeeds2fa(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(mockStoreNeeds2faHTTPCode)
+ io.WriteString(w, mockStoreNeeds2fa)
+ }))
+ defer mockServer.Close()
+ UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
+
+ discharge, err := dischargeAuthCaveat("third-party-caveat", "foo@example.com", "passwd", "")
+ c.Assert(err, Equals, ErrAuthenticationNeeds2fa)
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestDischargeAuthCaveatFails2fa(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(mockStore2faFailedHTTPCode)
+ io.WriteString(w, mockStore2faFailedResponse)
+ }))
+ defer mockServer.Close()
+ UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
+
+ discharge, err := dischargeAuthCaveat("third-party-caveat", "foo@example.com", "passwd", "")
+ c.Assert(err, Equals, Err2faFailed)
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestDischargeAuthCaveatInvalidLogin(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(mockStoreInvalidLoginCode)
+ io.WriteString(w, mockStoreInvalidLogin)
+ }))
+ defer mockServer.Close()
+ UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
+
+ discharge, err := dischargeAuthCaveat("third-party-caveat", "foo@example.com", "passwd", "")
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: Provided email/password is not correct.")
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestDischargeAuthCaveatMissingData(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnNoMacaroon)
+ }))
+ defer mockServer.Close()
+ UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
+
+ discharge, err := dischargeAuthCaveat("third-party-caveat", "foo@example.com", "passwd", "")
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: empty macaroon returned")
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestDischargeAuthCaveatError(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(500)
+ }))
+ defer mockServer.Close()
+ UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
+
+ discharge, err := dischargeAuthCaveat("third-party-caveat", "foo@example.com", "passwd", "")
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: server returned status 500")
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestRefreshDischargeMacaroon(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnDischarge)
+ }))
+ defer mockServer.Close()
+ UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
+
+ discharge, err := refreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon")
+ c.Assert(err, IsNil)
+ c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data")
+}
+
+func (s *authTestSuite) TestRefreshDischargeMacaroonInvalidLogin(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(mockStoreInvalidLoginCode)
+ io.WriteString(w, mockStoreInvalidLogin)
+ }))
+ defer mockServer.Close()
+ UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
+
+ discharge, err := refreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon")
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: Provided email/password is not correct.")
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestRefreshDischargeMacaroonMissingData(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnNoMacaroon)
+ }))
+ defer mockServer.Close()
+ UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
+
+ discharge, err := refreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon")
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: empty macaroon returned")
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestRefreshDischargeMacaroonError(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(500)
+ }))
+ defer mockServer.Close()
+ UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
+
+ discharge, err := refreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon")
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: server returned status 500")
+ c.Assert(discharge, Equals, "")
+}
+
+func (s *authTestSuite) TestLoginCaveatIDReturnCaveatID(c *C) {
+ m, err := macaroon.New([]byte("secret"), "some-id", "location")
+ c.Check(err, IsNil)
+ err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", UbuntuoneLocation)
+ c.Check(err, IsNil)
+
+ caveat, err := loginCaveatID(m)
+ c.Check(err, IsNil)
+ c.Check(caveat, Equals, "third-party-caveat")
+}
+
+func (s *authTestSuite) TestLoginCaveatIDMacaroonMissingCaveat(c *C) {
+ m, err := macaroon.New([]byte("secret"), "some-id", "location")
+ c.Check(err, IsNil)
+ err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", "other-location")
+ c.Check(err, IsNil)
+
+ caveat, err := loginCaveatID(m)
+ c.Check(err, NotNil)
+ c.Check(caveat, Equals, "")
+}
+
+func (s *authTestSuite) TestRequestStoreDeviceNonce(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnNonce)
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
+
+ nonce, err := requestStoreDeviceNonce()
+ c.Assert(err, IsNil)
+ c.Assert(nonce, Equals, "the-nonce")
+}
+
+func (s *authTestSuite) TestRequestStoreDeviceNonceEmptyResponse(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnNoNonce)
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
+
+ nonce, err := requestStoreDeviceNonce()
+ c.Assert(err, ErrorMatches, "cannot get nonce from store: empty nonce returned")
+ c.Assert(nonce, Equals, "")
+}
+
+func (s *authTestSuite) TestRequestStoreDeviceNonceError(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(500)
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
+
+ nonce, err := requestStoreDeviceNonce()
+ c.Assert(err, ErrorMatches, "cannot get nonce from store: store server returned status 500")
+ c.Assert(nonce, Equals, "")
+}
+
+func (s *authTestSuite) TestRequestDeviceSession(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(jsonReq), Equals, `{"device-session-request":"session-request","serial-assertion":"serial-assertion"}`)
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
+
+ io.WriteString(w, mockStoreReturnMacaroon)
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
+
+ macaroon, err := requestDeviceSession("serial-assertion", "session-request", "")
+ c.Assert(err, IsNil)
+ c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data")
+}
+
+func (s *authTestSuite) TestRequestDeviceSessionWithPreviousSession(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(jsonReq), Equals, `{"device-session-request":"session-request","serial-assertion":"serial-assertion"}`)
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="previous-session"`)
+
+ io.WriteString(w, mockStoreReturnMacaroon)
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
+
+ macaroon, err := requestDeviceSession("serial-assertion", "session-request", "previous-session")
+ c.Assert(err, IsNil)
+ c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data")
+}
+
+func (s *authTestSuite) TestRequestDeviceSessionMissingData(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, mockStoreReturnNoMacaroon)
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
+
+ macaroon, err := requestDeviceSession("serial-assertion", "session-request", "")
+ c.Assert(err, ErrorMatches, "cannot get device session from store: empty session returned")
+ c.Assert(macaroon, Equals, "")
+}
+
+func (s *authTestSuite) TestRequestDeviceSessionError(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(500)
+ w.Write([]byte("error body"))
+ }))
+ defer mockServer.Close()
+ MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
+
+ macaroon, err := requestDeviceSession("serial-assertion", "session-request", "")
+ c.Assert(err, ErrorMatches, `cannot get device session from store: store server returned status 500 and body "error body"`)
+ c.Assert(macaroon, Equals, "")
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "github.com/snapcore/snapd/snap"
+)
+
+// snapDetails encapsulates the data sent to us from the store as JSON.
+type snapDetails struct {
+ AnonDownloadURL string `json:"anon_download_url,omitempty"`
+ Architectures []string `json:"architecture"`
+ Channel string `json:"channel,omitempty"`
+ DownloadSha3_384 string `json:"download_sha3_384,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ Deltas []snapDeltaDetail `json:"deltas,omitempty"`
+ DownloadSize int64 `json:"binary_filesize,omitempty"`
+ DownloadURL string `json:"download_url,omitempty"`
+ Epoch string `json:"epoch"`
+ IconURL string `json:"icon_url"`
+ LastUpdated string `json:"last_updated,omitempty"`
+ Name string `json:"package_name"`
+ Prices map[string]float64 `json:"prices,omitempty"`
+ Publisher string `json:"publisher,omitempty"`
+ RatingsAverage float64 `json:"ratings_average,omitempty"`
+ Revision int `json:"revision"` // store revisions are ints starting at 1
+ ScreenshotURLs []string `json:"screenshot_urls,omitempty"`
+ SnapID string `json:"snap_id"`
+ SupportURL string `json:"support_url"`
+ Title string `json:"title"`
+ Type snap.Type `json:"content,omitempty"`
+ Version string `json:"version"`
+
+ // TODO: have the store return a 'developer_username' for this
+ Developer string `json:"origin"`
+ DeveloperID string `json:"developer_id"`
+
+ Private bool `json:"private"`
+ Confinement string `json:"confinement"`
+}
+
+type snapDeltaDetail struct {
+ FromRevision int `json:"from_revision"`
+ ToRevision int `json:"to_revision"`
+ Format string `json:"format"`
+ AnonDownloadURL string `json:"anon_download_url,omitempty"`
+ DownloadURL string `json:"download_url,omitempty"`
+ Size int64 `json:"binary_filesize,omitempty"`
+ Sha3_384 string `json:"download_sha3_384,omitempty"`
+}
+
+// channelSnapInfoDetails is the subset of snapDetails we need to get
+// information about the snaps in the various channels
+type channelSnapInfoDetails struct {
+ Revision int `json:"revision"` // store revisions are ints starting at 1
+ Confinement string `json:"confinement"`
+ Version string `json:"version"`
+ Channel string `json:"channel"`
+ Epoch string `json:"epoch"`
+ DownloadSize int64 `json:"binary_filesize"`
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+var (
+ // ErrEmptyQuery is returned from Find when the query, stripped of any prefixes, is empty.
+ ErrEmptyQuery = errors.New("empty query")
+
+ // ErrBadQuery is returned from Find when the query has special characters in strange places.
+ ErrBadQuery = errors.New("bad query")
+
+ // ErrSnapNotFound is returned when a snap can not be found
+ ErrSnapNotFound = errors.New("snap not found")
+
+ // ErrUnauthenticated is returned when authentication is needed to complete the query
+ ErrUnauthenticated = errors.New("you need to log in first")
+
+ // ErrAuthenticationNeeds2fa is returned if the authentication needs 2factor
+ ErrAuthenticationNeeds2fa = errors.New("two factor authentication required")
+
+ // Err2faFailed is returned when 2fa failed (e.g., a bad token was given)
+ Err2faFailed = errors.New("two factor authentication failed")
+
+ // ErrInvalidCredentials is returned on login error
+ ErrInvalidCredentials = errors.New("invalid credentials")
+
+ // ErrTOSNotAccepted is returned when the user has not accepted the store's terms of service.
+ ErrTOSNotAccepted = errors.New("terms of service not accepted")
+
+ // ErrNoPaymentMethods is returned when the user has no valid payment methods associated with their account.
+ ErrNoPaymentMethods = errors.New("no payment methods")
+
+ // ErrPaymentDeclined is returned when the user's payment method was declined by the upstream payment provider.
+ ErrPaymentDeclined = errors.New("payment declined")
+)
+
+// ErrDownload represents a download error
+type ErrDownload struct {
+ Code int
+ URL *url.URL
+}
+
+func (e *ErrDownload) Error() string {
+ return fmt.Sprintf("received an unexpected http response code (%v) when trying to download %s", e.Code, e.URL)
+}
+
+// ErrInvalidAuthData signals that the authentication data didn't pass validation.
+type ErrInvalidAuthData map[string][]string
+
+func (e ErrInvalidAuthData) Error() string {
+ var es []string
+ for _, v := range e {
+ es = append(es, v...)
+ }
+ // XXX: confirm with server people that extra args are all
+ // full sentences (with periods and capitalization)
+ // (empirically this checks out)
+ return strings.Join(es, " ")
+}
+
+// AssertionNotFoundError is returned when an assertion can not be found
+type AssertionNotFoundError struct {
+ Ref *asserts.Ref
+}
+
+func (e *AssertionNotFoundError) Error() string {
+ return fmt.Sprintf("%v not found", e.Ref)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "github.com/snapcore/snapd/testutil"
+
+ "gopkg.in/retry.v1"
+)
+
+var GetFlags = (*LoggedTransport).getFlags
+
+// MockDefaultRetryStrategy mocks the retry strategy used by several store requests
+func MockDefaultRetryStrategy(t *testutil.BaseTest, strategy retry.Strategy) {
+ originalDefaultRetryStrategy := defaultRetryStrategy
+ defaultRetryStrategy = strategy
+ t.AddCleanup(func() {
+ defaultRetryStrategy = originalDefaultRetryStrategy
+ })
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "errors"
+ "net/http"
+ "net/http/httputil"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/snapcore/snapd/logger"
+)
+
+type debugflag uint
+
+// set these via the Key environ
+const (
+ DebugRequest = debugflag(1 << iota)
+ DebugResponse
+ DebugBody
+)
+
+func (f debugflag) debugRequest() bool {
+ return f&DebugRequest != 0
+}
+
+func (f debugflag) debugResponse() bool {
+ return f&DebugResponse != 0
+}
+
+func (f debugflag) debugBody() bool {
+ return f&DebugBody != 0
+}
+
+// LoggedTransport is an http.RoundTripper that can be used by
+// http.Client to log request/response roundtrips.
+type LoggedTransport struct {
+ Transport http.RoundTripper
+ Key string
+ body bool
+}
+
+// RoundTrip is from the http.RoundTripper interface.
+func (tr *LoggedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ flags := tr.getFlags()
+
+ if flags.debugRequest() {
+ buf, _ := httputil.DumpRequestOut(req, tr.body && flags.debugBody())
+ logger.Debugf("> %q", buf)
+ }
+
+ rsp, err := tr.Transport.RoundTrip(req)
+
+ if err == nil && flags.debugResponse() {
+ buf, _ := httputil.DumpResponse(rsp, tr.body && flags.debugBody())
+ logger.Debugf("< %q", buf)
+ }
+
+ return rsp, err
+}
+
+func (tr *LoggedTransport) getFlags() debugflag {
+ flags, err := strconv.Atoi(os.Getenv(tr.Key))
+ if err != nil {
+ flags = 0
+ }
+
+ return debugflag(flags)
+}
+
+type httpClientOpts struct {
+ Timeout time.Duration
+ MayLogBody bool
+}
+
+// returns a new http.Client with a LoggedTransport, a Timeout and preservation
+// of range requests across redirects
+func newHTTPClient(opts *httpClientOpts) *http.Client {
+ if opts == nil {
+ opts = &httpClientOpts{}
+ }
+
+ return &http.Client{
+ Transport: &LoggedTransport{
+ Transport: http.DefaultTransport,
+ Key: "SNAPD_DEBUG_HTTP",
+ body: opts.MayLogBody,
+ },
+ Timeout: opts.Timeout,
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ if len(via) > 10 {
+ return errors.New("stopped after 10 redirects")
+ }
+ // preserve the range header across redirects
+ // to the CDN
+ v := via[0].Header.Get("Range")
+ req.Header.Set("Range", v)
+ return nil
+ },
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store_test
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strings"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type loggerSuite struct {
+ logbuf *bytes.Buffer
+}
+
+var _ = check.Suite(&loggerSuite{})
+
+func (loggerSuite) TearDownTest(c *check.C) {
+ os.Unsetenv("SNAPD_DEBUG")
+}
+
+func (s *loggerSuite) SetUpTest(c *check.C) {
+ os.Setenv("SNAPD_DEBUG", "true")
+ s.logbuf = bytes.NewBuffer(nil)
+ l, err := logger.NewConsoleLog(s.logbuf, logger.DefaultFlags)
+ c.Assert(err, check.IsNil)
+ logger.SetLogger(l)
+}
+
+func (loggerSuite) TestFlags(c *check.C) {
+ for _, f := range []interface{}{
+ store.DebugRequest,
+ store.DebugResponse,
+ store.DebugBody,
+ store.DebugRequest | store.DebugResponse | store.DebugBody,
+ } {
+ os.Setenv("TEST_FOO", fmt.Sprintf("%d", f))
+ tr := &store.LoggedTransport{
+ Key: "TEST_FOO",
+ }
+
+ c.Check(store.GetFlags(tr), check.Equals, f)
+ }
+}
+
+type fakeTransport struct {
+ req *http.Request
+ rsp *http.Response
+}
+
+func (t *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ t.req = req
+ return t.rsp, nil
+}
+
+func (s loggerSuite) TestLogging(c *check.C) {
+ req, err := http.NewRequest("WAT", "http://example.com/", nil)
+ c.Assert(err, check.IsNil)
+ rsp := &http.Response{
+ Status: "999 WAT",
+ StatusCode: 999,
+ }
+ tr := &store.LoggedTransport{
+ Transport: &fakeTransport{
+ rsp: rsp,
+ },
+ Key: "TEST_FOO",
+ }
+
+ os.Setenv("TEST_FOO", "7")
+
+ aRsp, err := tr.RoundTrip(req)
+ c.Assert(err, check.IsNil)
+ c.Check(aRsp, check.Equals, rsp)
+ c.Check(s.logbuf.String(), check.Matches, `(?ms).*> "WAT / HTTP/\S+.*`)
+ c.Check(s.logbuf.String(), check.Matches, `(?ms).*< "HTTP/\S+ 999 WAT.*`)
+}
+
+func (s loggerSuite) TestNotLoggingOctetStream(c *check.C) {
+ req, err := http.NewRequest("GET", "http://example.com/data", nil)
+ c.Assert(err, check.IsNil)
+ needle := "lots of binary data"
+ rsp := &http.Response{
+ Status: "200 OK",
+ StatusCode: 200,
+ Header: http.Header{
+ "Content-Type": []string{"application/octet-stream"},
+ },
+ Body: ioutil.NopCloser(strings.NewReader(needle)),
+ }
+ tr := &store.LoggedTransport{
+ Transport: &fakeTransport{
+ rsp: rsp,
+ },
+ Key: "TEST_FOO",
+ }
+
+ os.Setenv("TEST_FOO", "7")
+
+ aRsp, err := tr.RoundTrip(req)
+ c.Assert(err, check.IsNil)
+ c.Check(aRsp, check.Equals, rsp)
+ c.Check(s.logbuf.String(), check.Matches, `(?ms).*> "GET /data HTTP/\S+.*`)
+ c.Check(s.logbuf.String(), check.Not(testutil.Contains), needle)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "gopkg.in/retry.v1"
+ "io"
+ "net"
+ "net/http"
+)
+
+func shouldRetryHttpResponse(attempt *retry.Attempt, resp *http.Response) bool {
+ return (resp.StatusCode == 500 || resp.StatusCode == 503) && attempt.More()
+}
+
+func shouldRetryError(attempt *retry.Attempt, err error) bool {
+ if !attempt.More() {
+ return false
+ }
+ if netErr, ok := err.(net.Error); ok {
+ return netErr.Timeout()
+ }
+ return err == io.ErrUnexpectedEOF || err == io.EOF
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package store has support to use the Ubuntu Store for querying and downloading of snaps, and the related services.
+package store
+
+import (
+ "bytes"
+ "crypto"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+
+ "golang.org/x/net/context"
+ "golang.org/x/net/context/ctxhttp"
+ "gopkg.in/retry.v1"
+)
+
+// TODO: better/shorter names are probably in order once fewer legacy places are using this
+
+const (
+ // halJsonContentType is the default accept value for store requests
+ halJsonContentType = "application/hal+json"
+ // jsonContentType is for store enpoints that don't support HAL
+ jsonContentType = "application/json"
+ // UbuntuCoreWireProtocol is the protocol level we support when
+ // communicating with the store. History:
+ // - "1": client supports squashfs snaps
+ UbuntuCoreWireProtocol = "1"
+)
+
+// UserAgent to send
+// xxx: this should actually be set per client request, and include the client user agent
+var userAgent = "unset"
+
+var isTesting bool
+
+func init() {
+ if osutil.GetenvBool("SNAPPY_TESTING") {
+ isTesting = true
+ }
+}
+
+func SetUserAgentFromVersion(version string, extraProds ...string) {
+ extras := make([]string, 1, 3)
+ extras[0] = "series " + release.Series
+ if release.OnClassic {
+ extras = append(extras, "classic")
+ }
+ if release.ReleaseInfo.ForceDevMode() {
+ extras = append(extras, "devmode")
+ }
+ if isTesting {
+ extras = append(extras, "testing")
+ }
+ extraProdStr := ""
+ if len(extraProds) != 0 {
+ extraProdStr = " " + strings.Join(extraProds, " ")
+ }
+ // xxx this assumes ReleaseInfo's ID and VersionID don't have weird characters
+ // (see rfc 7231 for values of weird)
+ // assumption checks out in practice, q.v. https://github.com/zyga/os-release-zoo
+ userAgent = fmt.Sprintf("snapd/%v (%s)%s %s/%s (%s)", version, strings.Join(extras, "; "), extraProdStr, release.ReleaseInfo.ID, release.ReleaseInfo.VersionID, string(arch.UbuntuArchitecture()))
+}
+
+func UserAgent() string {
+ return userAgent
+}
+
+func infoFromRemote(d snapDetails) *snap.Info {
+ info := &snap.Info{}
+ info.Architectures = d.Architectures
+ info.Type = d.Type
+ info.Version = d.Version
+ info.Epoch = "0"
+ info.RealName = d.Name
+ info.SnapID = d.SnapID
+ info.Revision = snap.R(d.Revision)
+ info.EditedSummary = d.Summary
+ info.EditedDescription = d.Description
+ info.PublisherID = d.DeveloperID
+ info.Publisher = d.Developer
+ info.Channel = d.Channel
+ info.Sha3_384 = d.DownloadSha3_384
+ info.Size = d.DownloadSize
+ info.IconURL = d.IconURL
+ info.AnonDownloadURL = d.AnonDownloadURL
+ info.DownloadURL = d.DownloadURL
+ info.Prices = d.Prices
+ info.Private = d.Private
+ info.Confinement = snap.ConfinementType(d.Confinement)
+
+ deltas := make([]snap.DeltaInfo, len(d.Deltas))
+ for i, d := range d.Deltas {
+ deltas[i] = snap.DeltaInfo{
+ FromRevision: d.FromRevision,
+ ToRevision: d.ToRevision,
+ Format: d.Format,
+ AnonDownloadURL: d.AnonDownloadURL,
+ DownloadURL: d.DownloadURL,
+ Size: d.Size,
+ Sha3_384: d.Sha3_384,
+ }
+ }
+ info.Deltas = deltas
+
+ screenshots := make([]snap.ScreenshotInfo, 0, len(d.ScreenshotURLs))
+ for _, url := range d.ScreenshotURLs {
+ screenshots = append(screenshots, snap.ScreenshotInfo{
+ URL: url,
+ })
+ }
+ info.Screenshots = screenshots
+
+ return info
+}
+
+// Config represents the configuration to access the snap store
+type Config struct {
+ SearchURI *url.URL
+ DetailsURI *url.URL
+ BulkURI *url.URL
+ AssertionsURI *url.URL
+ OrdersURI *url.URL
+ CustomersMeURI *url.URL
+ SectionsURI *url.URL
+
+ // StoreID is the store id used if we can't get one through the AuthContext.
+ StoreID string
+
+ Architecture string
+ Series string
+
+ DetailFields []string
+ DeltaFormat string
+}
+
+// Store represents the ubuntu snap store
+type Store struct {
+ searchURI *url.URL
+ detailsURI *url.URL
+ bulkURI *url.URL
+ assertionsURI *url.URL
+ ordersURI *url.URL
+ customersMeURI *url.URL
+ sectionsURI *url.URL
+
+ architecture string
+ series string
+
+ noCDN bool
+
+ fallbackStoreID string
+
+ detailFields []string
+ deltaFormat string
+ // reused http client
+ client *http.Client
+
+ authContext auth.AuthContext
+
+ mu sync.Mutex
+ suggestedCurrency string
+}
+
+var defaultRetryStrategy = retry.LimitCount(5, retry.LimitTime(10*time.Second,
+ retry.Exponential{
+ Initial: 100 * time.Millisecond,
+ Factor: 2.5,
+ },
+))
+
+func respToError(resp *http.Response, msg string) error {
+ tpl := "cannot %s: got unexpected HTTP status code %d via %s to %q"
+ if oops := resp.Header.Get("X-Oops-Id"); oops != "" {
+ tpl += " [%s]"
+ return fmt.Errorf(tpl, msg, resp.StatusCode, resp.Request.Method, resp.Request.URL, oops)
+ }
+
+ return fmt.Errorf(tpl, msg, resp.StatusCode, resp.Request.Method, resp.Request.URL)
+}
+
+func getStructFields(s interface{}) []string {
+ st := reflect.TypeOf(s)
+ num := st.NumField()
+ fields := make([]string, 0, num)
+ for i := 0; i < num; i++ {
+ tag := st.Field(i).Tag.Get("json")
+ idx := strings.IndexRune(tag, ',')
+ if idx > -1 {
+ tag = tag[:idx]
+ }
+ if tag != "" {
+ fields = append(fields, tag)
+ }
+ }
+
+ return fields
+}
+
+func useDeltas() bool {
+ return osutil.GetenvBool("SNAPD_USE_DELTAS_EXPERIMENTAL")
+}
+
+func useStaging() bool {
+ return osutil.GetenvBool("SNAPPY_USE_STAGING_STORE")
+}
+
+func cpiURL() string {
+ // FIXME: this will become a store-url assertion
+ if u := os.Getenv("SNAPPY_FORCE_CPI_URL"); u != "" {
+ return u
+ }
+ if useStaging() {
+ return "https://search.apps.staging.ubuntu.com/api/v1/"
+ }
+
+ return "https://search.apps.ubuntu.com/api/v1/"
+}
+
+func authLocation() string {
+ if useStaging() {
+ return "login.staging.ubuntu.com"
+ }
+ return "login.ubuntu.com"
+}
+
+func authURL() string {
+ if u := os.Getenv("SNAPPY_FORCE_SSO_URL"); u != "" {
+ return u
+ }
+ return "https://" + authLocation() + "/api/v2"
+}
+
+func assertsURL() string {
+ if u := os.Getenv("SNAPPY_FORCE_SAS_URL"); u != "" {
+ return u
+ }
+ if useStaging() {
+ return "https://assertions.staging.ubuntu.com/v1/"
+ }
+
+ return "https://assertions.ubuntu.com/v1/"
+}
+
+func myappsURL() string {
+ if useStaging() {
+ return "https://myapps.developer.staging.ubuntu.com/"
+ }
+ return "https://myapps.developer.ubuntu.com/"
+}
+
+var defaultConfig = Config{}
+
+// DefaultConfig returns a copy of the default configuration ready to be adapted.
+func DefaultConfig() *Config {
+ cfg := defaultConfig
+ return &cfg
+}
+
+func init() {
+ storeBaseURI, err := url.Parse(cpiURL())
+ if err != nil {
+ panic(err)
+ }
+
+ defaultConfig.SearchURI, err = storeBaseURI.Parse("snaps/search")
+ if err != nil {
+ panic(err)
+ }
+
+ // slash at the end because snap name is appended to this with .Parse(snapName)
+ defaultConfig.DetailsURI, err = storeBaseURI.Parse("snaps/details/")
+ if err != nil {
+ panic(err)
+ }
+
+ defaultConfig.BulkURI, err = storeBaseURI.Parse("snaps/metadata")
+ if err != nil {
+ panic(err)
+ }
+
+ assertsBaseURI, err := url.Parse(assertsURL())
+ if err != nil {
+ panic(err)
+ }
+
+ defaultConfig.AssertionsURI, err = assertsBaseURI.Parse("assertions/")
+ if err != nil {
+ panic(err)
+ }
+
+ defaultConfig.OrdersURI, err = url.Parse(myappsURL() + "purchases/v1/orders")
+ if err != nil {
+ panic(err)
+ }
+
+ defaultConfig.CustomersMeURI, err = url.Parse(myappsURL() + "purchases/v1/customers/me")
+ if err != nil {
+ panic(err)
+ }
+
+ defaultConfig.SectionsURI, err = storeBaseURI.Parse("snaps/sections")
+ if err != nil {
+ panic(err)
+ }
+}
+
+type searchResults struct {
+ Payload struct {
+ Packages []snapDetails `json:"clickindex:package"`
+ } `json:"_embedded"`
+}
+
+type sectionResults struct {
+ Payload struct {
+ Sections []struct{ Name string } `json:"clickindex:sections"`
+ } `json:"_embedded"`
+}
+
+// The fields we are interested in
+var detailFields = getStructFields(snapDetails{})
+
+// The fields we are interested in for snap.ChannelSnapInfos
+var channelSnapInfoFields = getStructFields(channelSnapInfoDetails{})
+
+// The default delta format if not configured.
+var defaultSupportedDeltaFormat = "xdelta3"
+
+// New creates a new Store with the given access configuration and for given the store id.
+func New(cfg *Config, authContext auth.AuthContext) *Store {
+ if cfg == nil {
+ cfg = &defaultConfig
+ }
+
+ fields := cfg.DetailFields
+ if fields == nil {
+ fields = detailFields
+ }
+
+ rawQuery := ""
+ if len(fields) > 0 {
+ v := url.Values{}
+ v.Set("fields", strings.Join(fields, ","))
+ rawQuery = v.Encode()
+ }
+
+ var searchURI *url.URL
+ if cfg.SearchURI != nil {
+ uri := *cfg.SearchURI
+ uri.RawQuery = rawQuery
+ searchURI = &uri
+ }
+
+ var detailsURI *url.URL
+ if cfg.DetailsURI != nil {
+ uri := *cfg.DetailsURI
+ uri.RawQuery = rawQuery
+ detailsURI = &uri
+ }
+
+ var sectionsURI *url.URL
+ if cfg.SectionsURI != nil {
+ sectionsURI = cfg.SectionsURI
+ }
+
+ architecture := arch.UbuntuArchitecture()
+ if cfg.Architecture != "" {
+ architecture = cfg.Architecture
+ }
+
+ series := release.Series
+ if cfg.Series != "" {
+ series = cfg.Series
+ }
+
+ deltaFormat := cfg.DeltaFormat
+ if deltaFormat == "" {
+ deltaFormat = defaultSupportedDeltaFormat
+ }
+
+ // see https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex
+ return &Store{
+ searchURI: searchURI,
+ detailsURI: detailsURI,
+ bulkURI: cfg.BulkURI,
+ assertionsURI: cfg.AssertionsURI,
+ ordersURI: cfg.OrdersURI,
+ customersMeURI: cfg.CustomersMeURI,
+ sectionsURI: sectionsURI,
+ series: series,
+ architecture: architecture,
+ noCDN: osutil.GetenvBool("SNAPPY_STORE_NO_CDN"),
+ fallbackStoreID: cfg.StoreID,
+ detailFields: fields,
+ authContext: authContext,
+ deltaFormat: deltaFormat,
+
+ client: newHTTPClient(&httpClientOpts{
+ Timeout: 10 * time.Second,
+ MayLogBody: true,
+ }),
+ }
+}
+
+// LoginUser logs user in the store and returns the authentication macaroons.
+func LoginUser(username, password, otp string) (string, string, error) {
+ macaroon, err := requestStoreMacaroon()
+ if err != nil {
+ return "", "", err
+ }
+ deserializedMacaroon, err := auth.MacaroonDeserialize(macaroon)
+ if err != nil {
+ return "", "", err
+ }
+
+ // get SSO 3rd party caveat, and request discharge
+ loginCaveat, err := loginCaveatID(deserializedMacaroon)
+ if err != nil {
+ return "", "", err
+ }
+
+ discharge, err := dischargeAuthCaveat(loginCaveat, username, password, otp)
+ if err != nil {
+ return "", "", err
+ }
+
+ return macaroon, discharge, nil
+}
+
+// hasStoreAuth returns true if given user has store macaroons setup
+func hasStoreAuth(user *auth.UserState) bool {
+ return user != nil && user.StoreMacaroon != ""
+}
+
+// authenticateUser will add the store expected Macaroon Authorization header for user
+func authenticateUser(r *http.Request, user *auth.UserState) {
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, `Macaroon root="%s"`, user.StoreMacaroon)
+
+ // deserialize root macaroon (we need its signature to do the discharge binding)
+ root, err := auth.MacaroonDeserialize(user.StoreMacaroon)
+ if err != nil {
+ logger.Debugf("cannot deserialize root macaroon: %v", err)
+ return
+ }
+
+ for _, d := range user.StoreDischarges {
+ // prepare discharge for request
+ discharge, err := auth.MacaroonDeserialize(d)
+ if err != nil {
+ logger.Debugf("cannot deserialize discharge macaroon: %v", err)
+ return
+ }
+ discharge.Bind(root.Signature())
+
+ serializedDischarge, err := auth.MacaroonSerialize(discharge)
+ if err != nil {
+ logger.Debugf("cannot re-serialize discharge macaroon: %v", err)
+ return
+ }
+ fmt.Fprintf(&buf, `, discharge="%s"`, serializedDischarge)
+ }
+ r.Header.Set("Authorization", buf.String())
+}
+
+// refreshDischarges will request refreshed discharge macaroons for the user
+func refreshDischarges(user *auth.UserState) ([]string, error) {
+ newDischarges := make([]string, len(user.StoreDischarges))
+ for i, d := range user.StoreDischarges {
+ discharge, err := auth.MacaroonDeserialize(d)
+ if err != nil {
+ return nil, err
+ }
+ if discharge.Location() != UbuntuoneLocation {
+ newDischarges[i] = d
+ continue
+ }
+
+ refreshedDischarge, err := refreshDischargeMacaroon(d)
+ if err != nil {
+ return nil, err
+ }
+ newDischarges[i] = refreshedDischarge
+ }
+ return newDischarges, nil
+}
+
+// refreshUser will refresh user discharge macaroon and update state
+func (s *Store) refreshUser(user *auth.UserState) error {
+ newDischarges, err := refreshDischarges(user)
+ if err != nil {
+ return err
+ }
+
+ if s.authContext != nil {
+ curUser, err := s.authContext.UpdateUserAuth(user, newDischarges)
+ if err != nil {
+ return err
+ }
+ // update in place
+ *user = *curUser
+ }
+
+ return nil
+}
+
+// refreshDeviceSession will set or refresh the device session in the state
+func (s *Store) refreshDeviceSession(device *auth.DeviceState) error {
+ if s.authContext == nil {
+ return fmt.Errorf("internal error: no authContext")
+ }
+
+ nonce, err := requestStoreDeviceNonce()
+ if err != nil {
+ return err
+ }
+
+ sessionRequest, serialAssertion, err := s.authContext.DeviceSessionRequest(nonce)
+ if err != nil {
+ return err
+ }
+
+ session, err := requestDeviceSession(string(serialAssertion), string(sessionRequest), device.SessionMacaroon)
+ if err != nil {
+ return err
+ }
+
+ curDevice, err := s.authContext.UpdateDeviceAuth(device, session)
+ if err != nil {
+ return err
+ }
+ // update in place
+ *device = *curDevice
+ return nil
+}
+
+// authenticateDevice will add the store expected Macaroon X-Device-Authorization header for device
+func authenticateDevice(r *http.Request, device *auth.DeviceState) {
+ if device.SessionMacaroon != "" {
+ r.Header.Set("X-Device-Authorization", fmt.Sprintf(`Macaroon root="%s"`, device.SessionMacaroon))
+ }
+}
+
+func (s *Store) setStoreID(r *http.Request) {
+ storeID := s.fallbackStoreID
+ if s.authContext != nil {
+ cand, err := s.authContext.StoreID(storeID)
+ if err != nil {
+ logger.Debugf("cannot get store ID from state: %v", err)
+ } else {
+ storeID = cand
+ }
+ }
+ if storeID != "" {
+ r.Header.Set("X-Ubuntu-Store", storeID)
+ }
+}
+
+// requestOptions specifies parameters for store requests.
+type requestOptions struct {
+ Method string
+ URL *url.URL
+ Accept string
+ ContentType string
+ ExtraHeaders map[string]string
+ Data []byte
+}
+
+func cancelled(ctx context.Context) bool {
+ select {
+ case <-ctx.Done():
+ return true
+ default:
+ return false
+ }
+}
+
+// retryRequestDecodeJSON calls retryRequest and decodes the response into either success or failure.
+func (s *Store) retryRequestDecodeJSON(ctx context.Context, client *http.Client, reqOptions *requestOptions, user *auth.UserState, success interface{}, failure interface{}) (resp *http.Response, err error) {
+ return s.retryRequest(ctx, client, reqOptions, user, func(ok bool, resp *http.Response) error {
+ result := success
+ if !ok {
+ result = failure
+ }
+ if result != nil {
+ return json.NewDecoder(resp.Body).Decode(result)
+ }
+ return nil
+ })
+}
+
+// retryRequest calls doRequest and decodes the response in a retry loop.
+func (s *Store) retryRequest(ctx context.Context, client *http.Client, reqOptions *requestOptions, user *auth.UserState, decode func(ok bool, resp *http.Response) error) (resp *http.Response, err error) {
+ var attempt *retry.Attempt
+ startTime := time.Now()
+ for attempt = retry.Start(defaultRetryStrategy, nil); attempt.Next(); {
+ if attempt.Count() > 1 {
+ delta := time.Since(startTime) / time.Millisecond
+ logger.Debugf("Retyring %s, attempt %d, delta time=%v ms", reqOptions.URL, attempt.Count(), delta)
+ }
+ if cancelled(ctx) {
+ return nil, ctx.Err()
+ }
+
+ resp, err = s.doRequest(ctx, client, reqOptions, user)
+ if err != nil {
+ if shouldRetryError(attempt, err) {
+ continue
+ }
+ break
+ }
+
+ if shouldRetryHttpResponse(attempt, resp) {
+ resp.Body.Close()
+ continue
+ } else {
+ ok := (resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated)
+ // always decode on success; decode failures only if body is not empty
+ if !ok && resp.ContentLength == 0 {
+ resp.Body.Close()
+ break
+ }
+ err = decode(ok, resp)
+ resp.Body.Close()
+ if err != nil {
+ if shouldRetryError(attempt, err) {
+ continue
+ } else {
+ return nil, err
+ }
+ }
+ }
+ // break out from retry loop
+ break
+ }
+
+ if attempt.Count() > 1 {
+ var status string
+ delta := time.Since(startTime) / time.Millisecond
+ if err != nil {
+ status = err.Error()
+ } else if resp != nil {
+ status = fmt.Sprintf("%d", resp.StatusCode)
+ }
+ logger.Debugf("The retry loop for %s finished after %d retries, delta time=%v ms, status: %s", reqOptions.URL, attempt.Count(), delta, status)
+ }
+
+ return resp, err
+}
+
+// doRequest does an authenticated request to the store handling a potential macaroon refresh required if needed
+func (s *Store) doRequest(ctx context.Context, client *http.Client, reqOptions *requestOptions, user *auth.UserState) (*http.Response, error) {
+ req, err := s.newRequest(reqOptions, user)
+ if err != nil {
+ return nil, err
+ }
+
+ var resp *http.Response
+ if ctx != nil {
+ resp, err = ctxhttp.Do(ctx, client, req)
+ } else {
+ resp, err = client.Do(req)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ wwwAuth := resp.Header.Get("WWW-Authenticate")
+ if resp.StatusCode == 401 {
+ refreshed := false
+ if user != nil && strings.Contains(wwwAuth, "needs_refresh=1") {
+ // refresh user
+ err = s.refreshUser(user)
+ if err != nil {
+ return nil, err
+ }
+ refreshed = true
+ }
+ if strings.Contains(wwwAuth, "refresh_device_session=1") {
+ // refresh device session
+ if s.authContext == nil {
+ return nil, fmt.Errorf("internal error: no authContext")
+ }
+ device, err := s.authContext.Device()
+ if err != nil {
+ return nil, err
+ }
+
+ err = s.refreshDeviceSession(device)
+ if err != nil {
+ return nil, err
+ }
+ refreshed = true
+ }
+ if refreshed {
+ // close previous response and retry
+ // TODO: make this non-recursive or add a recursion limit
+ resp.Body.Close()
+ return s.doRequest(ctx, client, reqOptions, user)
+ }
+ }
+
+ return resp, err
+}
+
+// build a new http.Request with headers for the store
+func (s *Store) newRequest(reqOptions *requestOptions, user *auth.UserState) (*http.Request, error) {
+ var body io.Reader
+ if reqOptions.Data != nil {
+ body = bytes.NewBuffer(reqOptions.Data)
+ }
+
+ req, err := http.NewRequest(reqOptions.Method, reqOptions.URL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ if s.authContext != nil {
+ device, err := s.authContext.Device()
+ if err != nil {
+ return nil, err
+ }
+ // we don't have a session yet but have a serial, try
+ // to get a session
+ if device.SessionMacaroon == "" && device.Serial != "" {
+ err = s.refreshDeviceSession(device)
+ if err == auth.ErrNoSerial {
+ // missing serial assertion, log and continue without device authentication
+ logger.Debugf("cannot set device session: %v", err)
+ }
+ if err != nil && err != auth.ErrNoSerial {
+ return nil, err
+ }
+ }
+ authenticateDevice(req, device)
+ }
+
+ // only set user authentication if user logged in to the store
+ if hasStoreAuth(user) {
+ authenticateUser(req, user)
+ }
+
+ req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("Accept", reqOptions.Accept)
+ req.Header.Set("X-Ubuntu-Architecture", s.architecture)
+ req.Header.Set("X-Ubuntu-Series", s.series)
+ req.Header.Set("X-Ubuntu-Classic", strconv.FormatBool(release.OnClassic))
+ req.Header.Set("X-Ubuntu-Wire-Protocol", UbuntuCoreWireProtocol)
+ req.Header.Set("X-Ubuntu-No-CDN", strconv.FormatBool(s.noCDN))
+
+ if reqOptions.ContentType != "" {
+ req.Header.Set("Content-Type", reqOptions.ContentType)
+ }
+
+ for header, value := range reqOptions.ExtraHeaders {
+ req.Header.Set(header, value)
+ }
+
+ s.setStoreID(req)
+
+ return req, nil
+}
+
+func (s *Store) extractSuggestedCurrency(resp *http.Response) {
+ suggestedCurrency := resp.Header.Get("X-Suggested-Currency")
+
+ if suggestedCurrency != "" {
+ s.mu.Lock()
+ s.suggestedCurrency = suggestedCurrency
+ s.mu.Unlock()
+ }
+}
+
+// ordersResult encapsulates the order data sent to us from the software center agent.
+//
+// {
+// "orders": [
+// {
+// "snap_id": "abcd1234efgh5678ijkl9012",
+// "currency": "USD",
+// "amount": "2.99",
+// "state": "Complete",
+// "refundable_until": null,
+// "purchase_date": "2016-09-20T15:00:00+00:00"
+// },
+// {
+// "snap_id": "abcd1234efgh5678ijkl9012",
+// "currency": null,
+// "amount": null,
+// "state": "Complete",
+// "refundable_until": null,
+// "purchase_date": "2016-09-20T15:00:00+00:00"
+// }
+// ]
+// }
+type ordersResult struct {
+ Orders []*order `json:"orders"`
+}
+
+type order struct {
+ SnapID string `json:"snap_id"`
+ Currency string `json:"currency"`
+ Amount string `json:"amount"`
+ State string `json:"state"`
+ RefundableUntil string `json:"refundable_until"`
+ PurchaseDate string `json:"purchase_date"`
+}
+
+// decorateOrders sets the MustBuy property of each snap in the given list according to the user's known orders.
+func (s *Store) decorateOrders(snaps []*snap.Info, channel string, user *auth.UserState) error {
+ // Mark every non-free snap as must buy until we know better.
+ hasPriced := false
+ for _, info := range snaps {
+ if len(info.Prices) != 0 {
+ info.MustBuy = true
+ hasPriced = true
+ }
+ }
+
+ if user == nil {
+ return nil
+ }
+
+ if !hasPriced {
+ return nil
+ }
+
+ var err error
+
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: s.ordersURI,
+ Accept: jsonContentType,
+ }
+ var result ordersResult
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &result, nil)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode == http.StatusUnauthorized {
+ // TODO handle token expiry and refresh
+ return ErrInvalidCredentials
+ }
+ if resp.StatusCode != http.StatusOK {
+ return respToError(resp, "obtain known orders from store")
+ }
+
+ // Make a map of the IDs of bought snaps
+ bought := make(map[string]bool)
+ for _, order := range result.Orders {
+ bought[order.SnapID] = true
+ }
+
+ for _, info := range snaps {
+ info.MustBuy = mustBuy(info.Prices, bought[info.SnapID])
+ }
+
+ return nil
+}
+
+// mustBuy determines if a snap requires a payment, based on if it is non-free and if the user has already bought it
+func mustBuy(prices map[string]float64, bought bool) bool {
+ if len(prices) == 0 {
+ // If the snap is free, then it doesn't need buying
+ return false
+ }
+
+ return !bought
+}
+
+// fakeChannels is a stopgap method of getting pseudo-channels until
+// the details endpoint provides the real thing for us. The main
+// difference between this pseudo one and the real thing is that a
+// channel can be closed, and we'll be oblivious to it.
+func (s *Store) fakeChannels(snapID string, user *auth.UserState) (map[string]*snap.ChannelSnapInfo, error) {
+ snaps := make([]currentSnapJson, 4)
+ for i, channel := range []string{"stable", "candidate", "beta", "edge"} {
+ snaps[i] = currentSnapJson{
+ SnapID: snapID,
+ Channel: channel,
+ // revision, confinement, epoch purposely left empty
+ }
+ }
+ jsonData, err := json.Marshal(metadataWrapper{
+ Snaps: snaps,
+ Fields: channelSnapInfoFields,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ reqOptions := &requestOptions{
+ Method: "POST",
+ URL: s.bulkURI,
+ Accept: halJsonContentType,
+ ContentType: jsonContentType,
+ Data: jsonData,
+ }
+
+ var results struct {
+ Payload struct {
+ ChannelSnapInfoDetails []*channelSnapInfoDetails `json:"clickindex:package"`
+ } `json:"_embedded"`
+ }
+
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &results, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, respToError(resp, "query the store for channel information")
+ }
+
+ channelInfos := make(map[string]*snap.ChannelSnapInfo, 4)
+ for _, item := range results.Payload.ChannelSnapInfoDetails {
+ channelInfos[item.Channel] = &snap.ChannelSnapInfo{
+ Revision: snap.R(item.Revision),
+ Confinement: snap.ConfinementType(item.Confinement),
+ Version: item.Version,
+ Channel: item.Channel,
+ Epoch: item.Epoch,
+ Size: item.DownloadSize,
+ }
+ }
+
+ return channelInfos, nil
+}
+
+// A SnapSpec describes a single snap wanted from SnapInfo
+type SnapSpec struct {
+ Name string
+ Channel string
+ Revision snap.Revision
+}
+
+// SnapInfo returns the snap.Info for the store-hosted snap matching the given spec, or an error.
+func (s *Store) SnapInfo(snapSpec SnapSpec, user *auth.UserState) (*snap.Info, error) {
+ // get the query before doing Parse, as that overwrites it
+ query := s.detailsURI.Query()
+ u, err := s.detailsURI.Parse(snapSpec.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ query.Set("channel", snapSpec.Channel)
+ if !snapSpec.Revision.Unset() {
+ query.Set("revision", snapSpec.Revision.String())
+ query.Set("channel", "")
+ }
+
+ u.RawQuery = query.Encode()
+
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: u,
+ Accept: halJsonContentType,
+ }
+
+ var remote snapDetails
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &remote, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // check statusCode
+ switch resp.StatusCode {
+ case http.StatusOK:
+ // OK
+ case http.StatusNotFound:
+ return nil, ErrSnapNotFound
+ default:
+ msg := fmt.Sprintf("get details for snap %q in channel %q", snapSpec.Name, snapSpec.Channel)
+ return nil, respToError(resp, msg)
+ }
+
+ info := infoFromRemote(remote)
+
+ // only get the channels when it makes sense as part of the reply
+ if info.SnapID != "" && snapSpec.Channel == "" && snapSpec.Revision.Unset() {
+ channels, err := s.fakeChannels(info.SnapID, user)
+ if err != nil {
+ logger.Noticef("cannot get channels: %v", err)
+ } else {
+ info.Channels = channels
+ }
+ }
+
+ err = s.decorateOrders([]*snap.Info{info}, snapSpec.Channel, user)
+ if err != nil {
+ logger.Noticef("cannot get user orders: %v", err)
+ }
+
+ s.extractSuggestedCurrency(resp)
+
+ return info, nil
+}
+
+// A Search is what you do in order to Find something
+type Search struct {
+ Query string
+ Section string
+ Private bool
+ Prefix bool
+}
+
+// Find finds (installable) snaps from the store, matching the
+// given Search.
+func (s *Store) Find(search *Search, user *auth.UserState) ([]*snap.Info, error) {
+ searchTerm := search.Query
+
+ if search.Private && user == nil {
+ return nil, ErrUnauthenticated
+ }
+
+ searchTerm = strings.TrimSpace(searchTerm)
+
+ // these characters might have special meaning on the search
+ // server, and don't form part of a reasonable search, so
+ // abort if they're included.
+ //
+ // "-" might also be special on the server, but it's also a
+ // valid part of a package name, so we let it pass
+ if strings.ContainsAny(searchTerm, `+=&|><!(){}[]^"~*?:\/`) {
+ return nil, ErrBadQuery
+ }
+
+ u := *s.searchURI // make a copy, so we can mutate it
+ q := u.Query()
+
+ if search.Private {
+ if search.Prefix {
+ // The store only supports "fuzzy" search for private snaps.
+ // See http://search.apps.ubuntu.com/docs/
+ return nil, ErrBadQuery
+ }
+
+ q.Set("private", "true")
+ }
+
+ if search.Prefix {
+ q.Set("name", searchTerm)
+ } else {
+ q.Set("q", searchTerm)
+ }
+ if search.Section != "" {
+ q.Set("section", search.Section)
+ }
+
+ if release.OnClassic {
+ q.Set("confinement", "strict,classic")
+ } else {
+ q.Set("confinement", "strict")
+ }
+ u.RawQuery = q.Encode()
+
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: &u,
+ Accept: halJsonContentType,
+ }
+
+ var searchData searchResults
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &searchData, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != 200 {
+ return nil, respToError(resp, "search")
+ }
+
+ if ct := resp.Header.Get("Content-Type"); ct != halJsonContentType {
+ return nil, fmt.Errorf("received an unexpected content type (%q) when trying to search via %q", ct, resp.Request.URL)
+ }
+
+ snaps := make([]*snap.Info, len(searchData.Payload.Packages))
+ for i, pkg := range searchData.Payload.Packages {
+ snaps[i] = infoFromRemote(pkg)
+ }
+
+ err = s.decorateOrders(snaps, "", user)
+ if err != nil {
+ logger.Noticef("cannot get user orders: %v", err)
+ }
+
+ s.extractSuggestedCurrency(resp)
+
+ return snaps, nil
+}
+
+// Sections retrieves the list of available store sections.
+func (s *Store) Sections(user *auth.UserState) ([]string, error) {
+ u := *s.sectionsURI // make a copy, so we can mutate it
+
+ q := u.Query()
+
+ u.RawQuery = q.Encode()
+
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: &u,
+ Accept: halJsonContentType,
+ }
+
+ var sectionData sectionResults
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, §ionData, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != 200 {
+ return nil, respToError(resp, "sections")
+ }
+
+ if ct := resp.Header.Get("Content-Type"); ct != halJsonContentType {
+ return nil, fmt.Errorf("received an unexpected content type (%q) when trying to retrieve the sections via %q", ct, resp.Request.URL)
+ }
+
+ var sectionNames []string
+ for _, s := range sectionData.Payload.Sections {
+ sectionNames = append(sectionNames, s.Name)
+ }
+
+ return sectionNames, nil
+}
+
+// RefreshCandidate contains information for the store about the currently
+// installed snap so that the store can decide what update we should see
+type RefreshCandidate struct {
+ SnapID string
+ Revision snap.Revision
+ Epoch string
+ Block []snap.Revision
+
+ // the desired channel
+ Channel string
+}
+
+// the exact bits that we need to send to the store
+type currentSnapJson struct {
+ SnapID string `json:"snap_id"`
+ Channel string `json:"channel"`
+ Revision int `json:"revision,omitempty"`
+ Epoch string `json:"epoch"`
+ Confinement string `json:"confinement"`
+}
+
+type metadataWrapper struct {
+ Snaps []currentSnapJson `json:"snaps"`
+ Fields []string `json:"fields"`
+}
+
+// ListRefresh returns the available updates for a list of snap identified by fullname with channel.
+func (s *Store) ListRefresh(installed []*RefreshCandidate, user *auth.UserState) (snaps []*snap.Info, err error) {
+
+ candidateMap := map[string]*RefreshCandidate{}
+ currentSnaps := make([]currentSnapJson, 0, len(installed))
+ for _, cs := range installed {
+ revision := cs.Revision.N
+ if !cs.Revision.Store() {
+ revision = 0
+ }
+ // the store gets confused if we send snaps without a snapid
+ // (like local ones)
+ if cs.SnapID == "" {
+ continue
+ }
+
+ currentSnaps = append(currentSnaps, currentSnapJson{
+ SnapID: cs.SnapID,
+ Channel: cs.Channel,
+ Epoch: cs.Epoch,
+ Revision: revision,
+ // confinement purposely left empty
+ })
+ candidateMap[cs.SnapID] = cs
+ }
+
+ // build input for the updates endpoint
+ jsonData, err := json.Marshal(metadataWrapper{
+ Snaps: currentSnaps,
+ Fields: s.detailFields,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ reqOptions := &requestOptions{
+ Method: "POST",
+ URL: s.bulkURI,
+ Accept: halJsonContentType,
+ ContentType: jsonContentType,
+ Data: jsonData,
+ }
+
+ if useDeltas() {
+ logger.Debugf("Deltas enabled. Adding header X-Ubuntu-Delta-Formats: %v", s.deltaFormat)
+ reqOptions.ExtraHeaders = map[string]string{
+ "X-Ubuntu-Delta-Formats": s.deltaFormat,
+ }
+ }
+
+ var updateData searchResults
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &updateData, nil)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, respToError(resp, "query the store for updates")
+ }
+
+ res := make([]*snap.Info, 0, len(updateData.Payload.Packages))
+ for _, rsnap := range updateData.Payload.Packages {
+ rrev := snap.R(rsnap.Revision)
+ cand := candidateMap[rsnap.SnapID]
+
+ // the store also gives us identical revisions, filter those
+ // out, we are not interested
+ if rrev == cand.Revision {
+ continue
+ }
+ // do not upgade to a version we rolledback back from
+ if findRev(rrev, cand.Block) {
+ continue
+ }
+ res = append(res, infoFromRemote(rsnap))
+ }
+
+ s.extractSuggestedCurrency(resp)
+
+ return res, nil
+}
+
+func findRev(needle snap.Revision, haystack []snap.Revision) bool {
+ for _, r := range haystack {
+ if needle == r {
+ return true
+ }
+ }
+ return false
+}
+
+type HashError struct {
+ name string
+ sha3_384 string
+ targetSha3_384 string
+}
+
+func (e HashError) Error() string {
+ return fmt.Sprintf("sha3-384 mismatch after patching %q: got %s but expected %s", e.name, e.sha3_384, e.targetSha3_384)
+}
+
+// Download downloads the snap addressed by download info and returns its
+// filename.
+// The file is saved in temporary storage, and should be removed
+// after use to prevent the disk from running out of space.
+func (s *Store) Download(ctx context.Context, name string, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error {
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
+ return err
+ }
+ if useDeltas() {
+ logger.Debugf("Available deltas returned by store: %v", downloadInfo.Deltas)
+ }
+ if useDeltas() && len(downloadInfo.Deltas) == 1 {
+ err := s.downloadAndApplyDelta(name, targetPath, downloadInfo, pbar, user)
+ if err == nil {
+ return nil
+ }
+ // We revert to normal downloads if there is any error.
+ logger.Noticef("Cannot download or apply deltas for %s: %v", name, err)
+ }
+
+ partialPath := targetPath + ".partial"
+ w, err := os.OpenFile(partialPath, os.O_RDWR|os.O_CREATE, 0644)
+ if err != nil {
+ return err
+ }
+ resume, err := w.Seek(0, os.SEEK_END)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := w.Close(); cerr != nil && err == nil {
+ err = cerr
+ }
+ if err != nil {
+ os.Remove(w.Name())
+ }
+ }()
+
+ url := downloadInfo.AnonDownloadURL
+ if url == "" || hasStoreAuth(user) {
+ url = downloadInfo.DownloadURL
+ }
+
+ err = download(ctx, name, downloadInfo.Sha3_384, url, user, s, w, resume, pbar)
+ // If sha3 checksum is incorrect and it was a resumed download, retry from scratch.
+ // Note that we will retry this way only once.
+ if _, ok := err.(HashError); ok && resume > 0 {
+ logger.Debugf("Error on resumed download: %v", err.Error())
+ err = w.Truncate(0)
+ if err != nil {
+ return err
+ }
+ _, err = w.Seek(0, os.SEEK_SET)
+ if err != nil {
+ return err
+ }
+ err = download(ctx, name, downloadInfo.Sha3_384, url, user, s, w, 0, pbar)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if err := os.Rename(w.Name(), targetPath); err != nil {
+ return err
+ }
+
+ return w.Sync()
+}
+
+// download writes an http.Request showing a progress.Meter
+var download = func(ctx context.Context, name, sha3_384, downloadURL string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ storeURL, err := url.Parse(downloadURL)
+ if err != nil {
+ return err
+ }
+
+ var finalErr error
+ for attempt := retry.Start(defaultRetryStrategy, nil); attempt.Next(); {
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: storeURL,
+ }
+ h := crypto.SHA3_384.New()
+
+ if resume > 0 {
+ reqOptions.ExtraHeaders = map[string]string{
+ "Range": fmt.Sprintf("bytes=%d-", resume),
+ }
+ // seed the sha3 with the already local file
+ if _, err := w.Seek(0, os.SEEK_SET); err != nil {
+ return err
+ }
+ n, err := io.Copy(h, w)
+ if err != nil {
+ return err
+ }
+ if n != resume {
+ return fmt.Errorf("resume offset wrong: %d != %d", resume, n)
+ }
+ }
+
+ if cancelled(ctx) {
+ return fmt.Errorf("The download has been cancelled: %s", ctx.Err())
+ }
+ var resp *http.Response
+ resp, finalErr = s.doRequest(ctx, newHTTPClient(nil), reqOptions, user)
+
+ if cancelled(ctx) {
+ return fmt.Errorf("The download has been cancelled: %s", ctx.Err())
+ }
+ if finalErr != nil {
+ if shouldRetryError(attempt, finalErr) {
+ continue
+ }
+ break
+ }
+
+ if shouldRetryHttpResponse(attempt, resp) {
+ resp.Body.Close()
+ continue
+ }
+
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case http.StatusOK, http.StatusPartialContent:
+ case http.StatusUnauthorized:
+ return fmt.Errorf("cannot download non-free snap without purchase")
+ default:
+ return &ErrDownload{Code: resp.StatusCode, URL: resp.Request.URL}
+ }
+
+ if pbar == nil {
+ pbar = &progress.NullProgress{}
+ }
+ pbar.Start(name, float64(resp.ContentLength))
+ mw := io.MultiWriter(w, h, pbar)
+ _, finalErr = io.Copy(mw, resp.Body)
+ pbar.Finished()
+ if finalErr != nil {
+ if shouldRetryError(attempt, finalErr) {
+ // error while downloading should resume
+ var seekerr error
+ resume, seekerr = w.Seek(0, os.SEEK_END)
+ if seekerr == nil {
+ continue
+ }
+ // if seek failed, then don't retry end return the original error
+ }
+ break
+ }
+
+ if cancelled(ctx) {
+ return fmt.Errorf("The download has been cancelled: %s", ctx.Err())
+ }
+
+ actualSha3 := fmt.Sprintf("%x", h.Sum(nil))
+ if sha3_384 != "" && sha3_384 != actualSha3 {
+ finalErr = HashError{name, actualSha3, sha3_384}
+ }
+ break
+ }
+ return finalErr
+}
+
+// downloadDelta downloads the delta for the preferred format, returning the path.
+func (s *Store) downloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState) error {
+
+ if len(downloadInfo.Deltas) != 1 {
+ return errors.New("store returned more than one download delta")
+ }
+
+ deltaInfo := downloadInfo.Deltas[0]
+
+ if deltaInfo.Format != s.deltaFormat {
+ return fmt.Errorf("store returned unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format)
+ }
+
+ url := deltaInfo.AnonDownloadURL
+ if url == "" || hasStoreAuth(user) {
+ url = deltaInfo.DownloadURL
+ }
+
+ return download(context.TODO(), deltaName, deltaInfo.Sha3_384, url, user, s, w, 0, pbar)
+}
+
+// applyDelta generates a target snap from a previously downloaded snap and a downloaded delta.
+var applyDelta = func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error {
+ snapBase := fmt.Sprintf("%s_%d.snap", name, deltaInfo.FromRevision)
+ snapPath := filepath.Join(dirs.SnapBlobDir, snapBase)
+
+ if !osutil.FileExists(snapPath) {
+ return fmt.Errorf("snap %q revision %d not found at %s", name, deltaInfo.FromRevision, snapPath)
+ }
+
+ if deltaInfo.Format != "xdelta3" {
+ return fmt.Errorf("cannot apply unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format)
+ }
+
+ partialTargetPath := targetPath + ".partial"
+
+ xdelta3Args := []string{"-d", "-s", snapPath, deltaPath, partialTargetPath}
+ cmd := exec.Command("xdelta3", xdelta3Args...)
+
+ if err := cmd.Run(); err != nil {
+ if err := os.Remove(partialTargetPath); err != nil {
+ logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err)
+ }
+ return err
+ }
+
+ bsha3_384, _, err := osutil.FileDigest(partialTargetPath, crypto.SHA3_384)
+ if err != nil {
+ return err
+ }
+ sha3_384 := fmt.Sprintf("%x", bsha3_384)
+ if targetSha3_384 != "" && sha3_384 != targetSha3_384 {
+ if err := os.Remove(partialTargetPath); err != nil {
+ logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err)
+ }
+ return HashError{name, sha3_384, targetSha3_384}
+ }
+
+ if err := os.Rename(partialTargetPath, targetPath); err != nil {
+ return osutil.CopyFile(partialTargetPath, targetPath, 0)
+ }
+
+ return nil
+}
+
+// downloadAndApplyDelta downloads and then applies the delta to the current snap.
+func (s *Store) downloadAndApplyDelta(name, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error {
+ deltaInfo := &downloadInfo.Deltas[0]
+
+ deltaPath := fmt.Sprintf("%s.%s-%d-to-%d.partial", targetPath, deltaInfo.Format, deltaInfo.FromRevision, deltaInfo.ToRevision)
+ deltaName := filepath.Base(deltaPath)
+
+ w, err := os.Create(deltaPath)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := w.Close(); cerr != nil && err == nil {
+ err = cerr
+ }
+ os.Remove(deltaPath)
+ }()
+
+ err = s.downloadDelta(deltaName, downloadInfo, w, pbar, user)
+ if err != nil {
+ return err
+ }
+
+ logger.Debugf("Successfully downloaded delta for %q at %s", name, deltaPath)
+ if err := applyDelta(name, deltaPath, deltaInfo, targetPath, downloadInfo.Sha3_384); err != nil {
+ return err
+ }
+
+ logger.Debugf("Successfully applied delta for %q at %s, saving %d bytes.", name, deltaPath, downloadInfo.Size-deltaInfo.Size)
+ return nil
+}
+
+type assertionSvcError struct {
+ Status int `json:"status"`
+ Type string `json:"type"`
+ Title string `json:"title"`
+ Detail string `json:"detail"`
+}
+
+// Assertion retrivies the assertion for the given type and primary key.
+func (s *Store) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) {
+ u, err := s.assertionsURI.Parse(path.Join(assertType.Name, path.Join(primaryKey...)))
+ if err != nil {
+ return nil, err
+ }
+ v := url.Values{}
+ v.Set("max-format", strconv.Itoa(assertType.MaxSupportedFormat()))
+ u.RawQuery = v.Encode()
+
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: u,
+ Accept: asserts.MediaType,
+ }
+
+ var asrt asserts.Assertion
+ resp, err := s.retryRequest(context.TODO(), s.client, reqOptions, user, func(ok bool, resp *http.Response) error {
+ var e error
+ if ok {
+ // decode assertion
+ dec := asserts.NewDecoder(resp.Body)
+ asrt, e = dec.Decode()
+ } else {
+ contentType := resp.Header.Get("Content-Type")
+ if contentType == jsonContentType || contentType == "application/problem+json" {
+ var svcErr assertionSvcError
+ dec := json.NewDecoder(resp.Body)
+ if e = dec.Decode(&svcErr); e != nil {
+ return fmt.Errorf("cannot decode assertion service error with HTTP status code %d: %v", resp.StatusCode, e)
+ }
+ if svcErr.Status == 404 {
+ return &AssertionNotFoundError{&asserts.Ref{Type: assertType, PrimaryKey: primaryKey}}
+ }
+ return fmt.Errorf("assertion service error: [%s] %q", svcErr.Title, svcErr.Detail)
+ }
+ }
+ return e
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != 200 {
+ return nil, respToError(resp, "fetch assertion")
+ }
+
+ return asrt, err
+}
+
+// SuggestedCurrency retrieves the cached value for the store's suggested currency
+func (s *Store) SuggestedCurrency() string {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.suggestedCurrency == "" {
+ return "USD"
+ }
+ return s.suggestedCurrency
+}
+
+// BuyOptions specifies parameters to buy from the store.
+type BuyOptions struct {
+ SnapID string `json:"snap-id"`
+ Price float64 `json:"price"`
+ Currency string `json:"currency"` // ISO 4217 code as string
+}
+
+// BuyResult holds the state of a buy attempt.
+type BuyResult struct {
+ State string `json:"state,omitempty"`
+}
+
+// orderInstruction holds data sent to the store for orders.
+type orderInstruction struct {
+ SnapID string `json:"snap_id"`
+ Amount string `json:"amount,omitempty"`
+ Currency string `json:"currency,omitempty"`
+}
+
+type storeError struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+}
+
+func (s *storeError) Error() string {
+ return s.Message
+}
+
+type storeErrors struct {
+ Errors []*storeError `json:"error_list"`
+}
+
+func (s *storeErrors) Error() string {
+ if len(s.Errors) == 0 {
+ return "internal error: empty store error used as an actual error"
+ }
+ return "store reported an error: " + s.Errors[0].Error()
+}
+
+func buyOptionError(message string) (*BuyResult, error) {
+ return nil, fmt.Errorf("cannot buy snap: %s", message)
+}
+
+// Buy sends a buy request for the specified snap.
+// Returns the state of the order: Complete, Cancelled.
+func (s *Store) Buy(options *BuyOptions, user *auth.UserState) (*BuyResult, error) {
+ if options.SnapID == "" {
+ return buyOptionError("snap ID missing")
+ }
+ if options.Price <= 0 {
+ return buyOptionError("invalid expected price")
+ }
+ if options.Currency == "" {
+ return buyOptionError("currency missing")
+ }
+ if user == nil {
+ return nil, ErrUnauthenticated
+ }
+
+ // FIXME Would really rather not to do this, and have the same meaningful errors from the POST to order.
+ err := s.ReadyToBuy(user)
+ if err != nil {
+ return nil, err
+ }
+
+ instruction := orderInstruction{
+ SnapID: options.SnapID,
+ Amount: fmt.Sprintf("%.2f", options.Price),
+ Currency: options.Currency,
+ }
+
+ jsonData, err := json.Marshal(instruction)
+ if err != nil {
+ return nil, err
+ }
+
+ reqOptions := &requestOptions{
+ Method: "POST",
+ URL: s.ordersURI,
+ Accept: jsonContentType,
+ ContentType: jsonContentType,
+ Data: jsonData,
+ }
+
+ var orderDetails order
+ var errorInfo storeErrors
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &orderDetails, &errorInfo)
+ if err != nil {
+ return nil, err
+ }
+
+ switch resp.StatusCode {
+ case http.StatusOK, http.StatusCreated:
+ // user already ordered or order successful
+ if orderDetails.State == "Cancelled" {
+ return buyOptionError("payment cancelled")
+ }
+
+ return &BuyResult{
+ State: orderDetails.State,
+ }, nil
+ case http.StatusBadRequest:
+ // Invalid price was specified, etc.
+ return buyOptionError(fmt.Sprintf("bad request: %v", errorInfo.Error()))
+ case http.StatusNotFound:
+ // Likely because snap ID doesn't exist.
+ return buyOptionError("server says not found (snap got removed?)")
+ case http.StatusPaymentRequired:
+ // Payment failed for some reason.
+ return nil, ErrPaymentDeclined
+ case http.StatusUnauthorized:
+ // TODO handle token expiry and refresh
+ return nil, ErrInvalidCredentials
+ default:
+ return nil, respToError(resp, fmt.Sprintf("buy snap: %v", errorInfo))
+ }
+}
+
+type storeCustomer struct {
+ LatestTOSDate string `json:"latest_tos_date"`
+ AcceptedTOSDate string `json:"accepted_tos_date"`
+ LatestTOSAccepted bool `json:"latest_tos_accepted"`
+ HasPaymentMethod bool `json:"has_payment_method"`
+}
+
+// ReadyToBuy returns nil if the user's account has accepted T&Cs and has a payment method registered, and an error otherwise
+func (s *Store) ReadyToBuy(user *auth.UserState) error {
+ if user == nil {
+ return ErrUnauthenticated
+ }
+
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: s.customersMeURI,
+ Accept: jsonContentType,
+ }
+
+ var customer storeCustomer
+ var errors storeErrors
+ resp, err := s.retryRequestDecodeJSON(context.TODO(), s.client, reqOptions, user, &customer, &errors)
+ if err != nil {
+ return err
+ }
+
+ switch resp.StatusCode {
+ case http.StatusOK:
+ if !customer.HasPaymentMethod {
+ return ErrNoPaymentMethods
+ }
+ if !customer.LatestTOSAccepted {
+ return ErrTOSNotAccepted
+ }
+ return nil
+ case http.StatusNotFound:
+ // Likely because user has no account registered on the pay server
+ return fmt.Errorf("cannot get customer details: server says no account exists")
+ case http.StatusUnauthorized:
+ return ErrInvalidCredentials
+ default:
+ if len(errors.Errors) == 0 {
+ return fmt.Errorf("cannot get customer details: unexpected HTTP code %d", resp.StatusCode)
+ }
+ return &errors
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "bytes"
+ "crypto"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+ . "gopkg.in/check.v1"
+ "gopkg.in/macaroon.v1"
+ "gopkg.in/retry.v1"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type remoteRepoTestSuite struct {
+ testutil.BaseTest
+ store *Store
+ logbuf *bytes.Buffer
+ user *auth.UserState
+ localUser *auth.UserState
+ device *auth.DeviceState
+
+ origDownloadFunc func(context.Context, string, string, string, *auth.UserState, *Store, io.ReadWriteSeeker, int64, progress.Meter) error
+ mockXDelta *testutil.MockCmd
+}
+
+func TestStore(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&remoteRepoTestSuite{})
+
+const (
+ exSerial = `type: serial
+authority-id: my-brand
+brand-id: my-brand
+model: baz-3000
+serial: 9999
+device-key:
+ AcbBTQRWhcGAARAAtJGIguK7FhSyRxL/6jvdy0zAgGCjC1xVNFzeF76p5G8BXNEEHZUHK+z8Gr2J
+ inVrpvhJhllf5Ob2dIMH2YQbC9jE1kjbzvuauQGDqk6tNQm0i3KDeHCSPgVN+PFXPwKIiLrh66Po
+ AC7OfR1rFUgCqu0jch0H6Nue0ynvEPiY4dPeXq7mCdpDr5QIAM41L+3hg0OdzvO8HMIGZQpdF6jP
+ 7fkkVMROYvHUOJ8kknpKE7FiaNNpH7jK1qNxOYhLeiioX0LYrdmTvdTWHrSKZc82ZmlDjpKc4hUx
+ VtTXMAysw7CzIdREPom/vJklnKLvZt+Wk5AEF5V5YKnuT3pY+fjVMZ56GtTEeO/Er/oLk/n2xUK5
+ fD5DAyW/9z0ygzwTbY5IuWXyDfYneL4nXwWOEgg37Z4+8mTH+ftTz2dl1x1KIlIR2xo0kxf9t8K+
+ jlr13vwF1+QReMCSUycUsZ2Eep5XhjI+LG7G1bMSGqodZTIOXLkIy6+3iJ8Z/feIHlJ0ELBDyFbl
+ Yy04Sf9LI148vJMsYenonkoWejWdMi8iCUTeaZydHJEUBU/RbNFLjCWa6NIUe9bfZgLiOOZkps54
+ +/AL078ri/tGjo/5UGvezSmwrEoWJyqrJt2M69N2oVDLJcHeo2bUYPtFC2Kfb2je58JrJ+llifdg
+ rAsxbnHXiXyVimUAEQEAAQ==
+device-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+timestamp: 2016-08-24T21:55:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=`
+
+ exDeviceSessionRequest = `type: device-session-request
+brand-id: my-brand
+model: baz-3000
+serial: 9999
+nonce: @NONCE@
+timestamp: 2016-08-24T21:55:00Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=`
+)
+
+type testAuthContext struct {
+ c *C
+ device *auth.DeviceState
+ user *auth.UserState
+
+ storeID string
+}
+
+func (ac *testAuthContext) Device() (*auth.DeviceState, error) {
+ freshDevice := *ac.device
+ return &freshDevice, nil
+}
+
+func (ac *testAuthContext) UpdateDeviceAuth(d *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) {
+ ac.c.Assert(d, DeepEquals, ac.device)
+ updated := *ac.device
+ updated.SessionMacaroon = newSessionMacaroon
+ *ac.device = updated
+ return &updated, nil
+}
+
+func (ac *testAuthContext) UpdateUserAuth(u *auth.UserState, newDischarges []string) (*auth.UserState, error) {
+ ac.c.Assert(u, DeepEquals, ac.user)
+ updated := *ac.user
+ updated.StoreDischarges = newDischarges
+ return &updated, nil
+}
+
+func (ac *testAuthContext) StoreID(fallback string) (string, error) {
+ if ac.storeID != "" {
+ return ac.storeID, nil
+ }
+ return fallback, nil
+}
+
+func (ac *testAuthContext) DeviceSessionRequest(nonce string) ([]byte, []byte, error) {
+ serial, err := asserts.Decode([]byte(exSerial))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sessReq, err := asserts.Decode([]byte(strings.Replace(exDeviceSessionRequest, "@NONCE@", nonce, 1)))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return asserts.Encode(sessReq.(*asserts.DeviceSessionRequest)), asserts.Encode(serial.(*asserts.Serial)), nil
+}
+
+func makeTestMacaroon() (*macaroon.Macaroon, error) {
+ m, err := macaroon.New([]byte("secret"), "some-id", "location")
+ if err != nil {
+ return nil, err
+ }
+ err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", UbuntuoneLocation)
+ if err != nil {
+ return nil, err
+ }
+
+ return m, nil
+}
+
+func makeTestDischarge() (*macaroon.Macaroon, error) {
+ m, err := macaroon.New([]byte("shared-key"), "third-party-caveat", UbuntuoneLocation)
+ if err != nil {
+ return nil, err
+ }
+
+ return m, nil
+}
+
+func makeTestRefreshDischargeResponse() (string, error) {
+ m, err := macaroon.New([]byte("shared-key"), "refreshed-third-party-caveat", UbuntuoneLocation)
+ if err != nil {
+ return "", err
+ }
+
+ return auth.MacaroonSerialize(m)
+}
+
+func createTestUser(userID int, root, discharge *macaroon.Macaroon) (*auth.UserState, error) {
+ serializedMacaroon, err := auth.MacaroonSerialize(root)
+ if err != nil {
+ return nil, err
+ }
+ serializedDischarge, err := auth.MacaroonSerialize(discharge)
+ if err != nil {
+ return nil, err
+ }
+
+ return &auth.UserState{
+ ID: userID,
+ Username: "test-user",
+ Macaroon: serializedMacaroon,
+ Discharges: []string{serializedDischarge},
+ StoreMacaroon: serializedMacaroon,
+ StoreDischarges: []string{serializedDischarge},
+ }, nil
+}
+
+func createTestDevice() *auth.DeviceState {
+ return &auth.DeviceState{
+ Brand: "some-brand",
+ SessionMacaroon: "device-macaroon",
+ Serial: "9999",
+ }
+}
+
+func (t *remoteRepoTestSuite) SetUpTest(c *C) {
+ t.store = New(nil, nil)
+ t.origDownloadFunc = download
+ dirs.SetRootDir(c.MkDir())
+ c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), IsNil)
+
+ t.logbuf = bytes.NewBuffer(nil)
+ l, err := logger.NewConsoleLog(t.logbuf, logger.DefaultFlags)
+ c.Assert(err, IsNil)
+ logger.SetLogger(l)
+
+ root, err := makeTestMacaroon()
+ c.Assert(err, IsNil)
+ discharge, err := makeTestDischarge()
+ c.Assert(err, IsNil)
+ t.user, err = createTestUser(1, root, discharge)
+ c.Assert(err, IsNil)
+ t.localUser = &auth.UserState{
+ ID: 11,
+ Username: "test-user",
+ Macaroon: "snapd-macaroon",
+ }
+ t.device = createTestDevice()
+ t.mockXDelta = testutil.MockCommand(c, "xdelta3", "")
+
+ MockDefaultRetryStrategy(&t.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second,
+ retry.Exponential{
+ Initial: 1 * time.Millisecond,
+ Factor: 1,
+ },
+ )))
+}
+
+func (t *remoteRepoTestSuite) TearDownTest(c *C) {
+ download = t.origDownloadFunc
+ t.mockXDelta.Restore()
+}
+
+func (t *remoteRepoTestSuite) TearDownSuite(c *C) {
+ logger.SimpleSetup()
+}
+
+func (t *remoteRepoTestSuite) expectedAuthorization(c *C, user *auth.UserState) string {
+ var buf bytes.Buffer
+
+ root, err := auth.MacaroonDeserialize(user.StoreMacaroon)
+ c.Assert(err, IsNil)
+ discharge, err := auth.MacaroonDeserialize(user.StoreDischarges[0])
+ c.Assert(err, IsNil)
+ discharge.Bind(root.Signature())
+
+ serializedMacaroon, err := auth.MacaroonSerialize(root)
+ c.Assert(err, IsNil)
+ serializedDischarge, err := auth.MacaroonSerialize(discharge)
+ c.Assert(err, IsNil)
+
+ fmt.Fprintf(&buf, `Macaroon root="%s", discharge="%s"`, serializedMacaroon, serializedDischarge)
+ return buf.String()
+}
+
+func (t *remoteRepoTestSuite) TestDownloadOK(c *C) {
+
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ c.Check(url, Equals, "anon-url")
+ w.Write([]byte("I was downloaded"))
+ return nil
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
+ c.Assert(err, IsNil)
+ defer os.Remove(path)
+
+ content, err := ioutil.ReadFile(path)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "I was downloaded")
+}
+
+func (t *remoteRepoTestSuite) TestDownloadRangeRequest(c *C) {
+ partialContentStr := "partial content "
+
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ c.Check(resume, Equals, int64(len(partialContentStr)))
+ c.Check(url, Equals, "anon-url")
+ w.Write([]byte("was downloaded"))
+ return nil
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+ snap.Sha3_384 = "abcdabcd"
+
+ targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
+ err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
+ c.Assert(err, IsNil)
+
+ err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
+ c.Assert(err, IsNil)
+
+ content, err := ioutil.ReadFile(targetFn)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, partialContentStr+"was downloaded")
+}
+
+func (t *remoteRepoTestSuite) TestDownloadRangeRequestRetryOnHashError(c *C) {
+ partialContentStr := "partial content "
+
+ n := 0
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ n++
+ if n == 1 {
+ // force sha3 error on first download
+ c.Check(resume, Equals, int64(len(partialContentStr)))
+ return HashError{"foo", "1234", "5678"}
+ }
+ w.Write([]byte("file was downloaded from scratch"))
+ return nil
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+ snap.Sha3_384 = ""
+
+ targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
+ err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
+ c.Assert(err, IsNil)
+
+ err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
+ c.Assert(err, IsNil)
+ c.Assert(n, Equals, 2)
+
+ content, err := ioutil.ReadFile(targetFn)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "file was downloaded from scratch")
+}
+
+func (t *remoteRepoTestSuite) TestDownloadRangeRequestFailOnHashError(c *C) {
+ partialContentStr := "partial content "
+
+ n := 0
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ n++
+ return HashError{"foo", "1234", "5678"}
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+ snap.Sha3_384 = ""
+
+ targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
+ err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
+ c.Assert(err, IsNil)
+
+ err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
+ c.Assert(err, NotNil)
+ c.Assert(err, ErrorMatches, `sha3-384 mismatch after patching "foo": got 1234 but expected 5678`)
+ c.Assert(n, Equals, 2)
+}
+
+func (t *remoteRepoTestSuite) TestAuthenticatedDownloadDoesNotUseAnonURL(c *C) {
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ // check user is pass and auth url is used
+ c.Check(user, Equals, t.user)
+ c.Check(url, Equals, "AUTH-URL")
+
+ w.Write([]byte("I was downloaded"))
+ return nil
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, t.user)
+ c.Assert(err, IsNil)
+ defer os.Remove(path)
+
+ content, err := ioutil.ReadFile(path)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "I was downloaded")
+}
+
+func (t *remoteRepoTestSuite) TestLocalUserDownloadUsesAnonURL(c *C) {
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ c.Check(url, Equals, "anon-url")
+
+ w.Write([]byte("I was downloaded"))
+ return nil
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, t.localUser)
+ c.Assert(err, IsNil)
+ defer os.Remove(path)
+
+ content, err := ioutil.ReadFile(path)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "I was downloaded")
+}
+
+func (t *remoteRepoTestSuite) TestDownloadFails(c *C) {
+ var tmpfile *os.File
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ tmpfile = w.(*os.File)
+ return fmt.Errorf("uh, it failed")
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+ // simulate a failed download
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
+ c.Assert(err, ErrorMatches, "uh, it failed")
+ // ... and ensure that the tempfile is removed
+ c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestDownloadSyncFails(c *C) {
+ var tmpfile *os.File
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ tmpfile = w.(*os.File)
+ w.Write([]byte("sync will fail"))
+ err := tmpfile.Close()
+ c.Assert(err, IsNil)
+ return nil
+ }
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+
+ // simulate a failed sync
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
+ c.Assert(err, ErrorMatches, "fsync:.*")
+ // ... and ensure that the tempfile is removed
+ c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestActualDownload(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ io.WriteString(w, "response-data")
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+ var buf SillyBuffer
+ // keep tests happy
+ sha3 := ""
+ err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil)
+ c.Assert(err, IsNil)
+ c.Check(buf.String(), Equals, "response-data")
+ c.Check(n, Equals, 1)
+}
+
+func (t *remoteRepoTestSuite) TestDownloadCancellation(c *C) {
+ // the channel used by mock server to request cancellation from the test
+ syncCh := make(chan struct{})
+
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ io.WriteString(w, "foo")
+ syncCh <- struct{}{}
+ io.WriteString(w, "bar")
+ time.Sleep(time.Duration(1) * time.Second)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ result := make(chan string)
+ go func() {
+ sha3 := ""
+ var buf SillyBuffer
+ err := download(ctx, "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil)
+ result <- err.Error()
+ close(result)
+ }()
+
+ <-syncCh
+ cancel()
+
+ err := <-result
+ c.Check(n, Equals, 1)
+ c.Assert(err, Equals, "The download has been cancelled: context canceled")
+}
+
+type nopeSeeker struct{ io.ReadWriter }
+
+func (nopeSeeker) Seek(int64, int) (int64, error) {
+ return -1, errors.New("what is this, quidditch?")
+}
+
+func (t *remoteRepoTestSuite) TestActualDownloadNonPurchased401(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusUnauthorized)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+ var buf bytes.Buffer
+ err := download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, nopeSeeker{&buf}, -1, nil)
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot download non-free snap without purchase")
+ c.Check(n, Equals, 1)
+}
+
+func (t *remoteRepoTestSuite) TestActualDownload404(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+ var buf SillyBuffer
+ err := download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil)
+ c.Assert(err, NotNil)
+ c.Assert(err, FitsTypeOf, &ErrDownload{})
+ c.Check(err.(*ErrDownload).Code, Equals, http.StatusNotFound)
+ c.Check(n, Equals, 1)
+}
+
+func (t *remoteRepoTestSuite) TestActualDownload500(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+ var buf SillyBuffer
+ err := download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil)
+ c.Assert(err, NotNil)
+ c.Assert(err, FitsTypeOf, &ErrDownload{})
+ c.Check(err.(*ErrDownload).Code, Equals, http.StatusInternalServerError)
+ c.Check(n, Equals, 5)
+}
+
+func (t *remoteRepoTestSuite) TestActualDownload500Once(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ if n == 1 {
+ w.WriteHeader(http.StatusInternalServerError)
+ } else {
+ io.WriteString(w, "response-data")
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+ var buf SillyBuffer
+ // keep tests happy
+ sha3 := ""
+ err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil)
+ c.Assert(err, IsNil)
+ c.Check(buf.String(), Equals, "response-data")
+ c.Check(n, Equals, 2)
+}
+
+// SillyBuffer is a ReadWriteSeeker buffer with a limited size for the tests
+// (bytes does not implement an ReadWriteSeeker)
+type SillyBuffer struct {
+ buf [1024]byte
+ pos int64
+ end int64
+}
+
+func NewSillyBufferString(s string) *SillyBuffer {
+ sb := &SillyBuffer{
+ pos: int64(len(s)),
+ end: int64(len(s)),
+ }
+ copy(sb.buf[0:], []byte(s))
+ return sb
+}
+func (sb *SillyBuffer) Read(b []byte) (n int, err error) {
+ if sb.pos >= int64(sb.end) {
+ return 0, io.EOF
+ }
+ n = copy(b, sb.buf[sb.pos:sb.end])
+ sb.pos += int64(n)
+ return n, nil
+}
+func (sb *SillyBuffer) Seek(offset int64, whence int) (int64, error) {
+ if whence != 0 {
+ panic("only io.SeekStart implemented in SillyBuffer")
+ }
+ if offset < 0 || offset > int64(sb.end) {
+ return 0, fmt.Errorf("seek out of bounds: %d", offset)
+ }
+ sb.pos = offset
+ return sb.pos, nil
+}
+func (sb *SillyBuffer) Write(p []byte) (n int, err error) {
+ n = copy(sb.buf[sb.pos:], p)
+ sb.pos += int64(n)
+ if sb.pos > sb.end {
+ sb.end = sb.pos
+ }
+ return n, nil
+}
+func (sb *SillyBuffer) String() string {
+ return string(sb.buf[0:sb.pos])
+}
+
+func (t *remoteRepoTestSuite) TestActualDownloadResume(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ io.WriteString(w, "data")
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ theStore := New(&Config{}, nil)
+ buf := NewSillyBufferString("some ")
+ // calc the expected hash
+ h := crypto.SHA3_384.New()
+ h.Write([]byte("some data"))
+ sha3 := fmt.Sprintf("%x", h.Sum(nil))
+ err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil)
+ c.Check(err, IsNil)
+ c.Check(buf.String(), Equals, "some data")
+ c.Check(n, Equals, 1)
+}
+
+type downloadBehaviour []struct {
+ url string
+ error bool
+}
+
+var deltaTests = []struct {
+ downloads downloadBehaviour
+ info snap.DownloadInfo
+ expectedContent string
+}{{
+ // The full snap is not downloaded, but rather the delta
+ // is downloaded and applied.
+ downloads: downloadBehaviour{
+ {url: "delta-url"},
+ },
+ info: snap.DownloadInfo{
+ AnonDownloadURL: "full-snap-url",
+ Deltas: []snap.DeltaInfo{
+ {AnonDownloadURL: "delta-url", Format: "xdelta3"},
+ },
+ },
+ expectedContent: "snap-content-via-delta",
+}, {
+ // If there is an error during the delta download, the
+ // full snap is downloaded as per normal.
+ downloads: downloadBehaviour{
+ {error: true},
+ {url: "full-snap-url"},
+ },
+ info: snap.DownloadInfo{
+ AnonDownloadURL: "full-snap-url",
+ Deltas: []snap.DeltaInfo{
+ {AnonDownloadURL: "delta-url", Format: "xdelta3"},
+ },
+ },
+ expectedContent: "full-snap-url-content",
+}, {
+ // If more than one matching delta is returned by the store
+ // we ignore deltas and do the full download.
+ downloads: downloadBehaviour{
+ {url: "full-snap-url"},
+ },
+ info: snap.DownloadInfo{
+ AnonDownloadURL: "full-snap-url",
+ Deltas: []snap.DeltaInfo{
+ {AnonDownloadURL: "delta-url", Format: "xdelta3"},
+ {AnonDownloadURL: "delta-url-2", Format: "xdelta3"},
+ },
+ },
+ expectedContent: "full-snap-url-content",
+}}
+
+func (t *remoteRepoTestSuite) TestDownloadWithDelta(c *C) {
+ origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
+ defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
+ c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
+
+ for _, testCase := range deltaTests {
+ downloadIndex := 0
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ if testCase.downloads[downloadIndex].error {
+ downloadIndex++
+ return errors.New("Bang")
+ }
+ c.Check(url, Equals, testCase.downloads[downloadIndex].url)
+ w.Write([]byte(testCase.downloads[downloadIndex].url + "-content"))
+ downloadIndex++
+ return nil
+ }
+ applyDelta = func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error {
+ c.Check(deltaInfo, Equals, &testCase.info.Deltas[0])
+ err := ioutil.WriteFile(targetPath, []byte("snap-content-via-delta"), 0644)
+ c.Assert(err, IsNil)
+ return nil
+ }
+
+ path := filepath.Join(c.MkDir(), "subdir", "downloaded-file")
+ err := t.store.Download(context.TODO(), "foo", path, &testCase.info, nil, nil)
+
+ c.Assert(err, IsNil)
+ defer os.Remove(path)
+ content, err := ioutil.ReadFile(path)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, testCase.expectedContent)
+ }
+}
+
+var downloadDeltaTests = []struct {
+ info snap.DownloadInfo
+ authenticated bool
+ useLocalUser bool
+ format string
+ expectedURL string
+ expectError bool
+}{{
+ // An unauthenticated request downloads the anonymous delta url.
+ info: snap.DownloadInfo{
+ Sha3_384: "sha3",
+ Deltas: []snap.DeltaInfo{
+ {AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
+ },
+ },
+ authenticated: false,
+ format: "xdelta3",
+ expectedURL: "anon-delta-url",
+ expectError: false,
+}, {
+ // An authenticated request downloads the authenticated delta url.
+ info: snap.DownloadInfo{
+ Sha3_384: "sha3",
+ Deltas: []snap.DeltaInfo{
+ {DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
+ },
+ },
+ authenticated: true,
+ useLocalUser: false,
+ format: "xdelta3",
+ expectedURL: "auth-delta-url",
+ expectError: false,
+}, {
+ // A local authenticated request downloads the anonymous delta url.
+ info: snap.DownloadInfo{
+ Sha3_384: "sha3",
+ Deltas: []snap.DeltaInfo{
+ {AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
+ },
+ },
+ authenticated: true,
+ useLocalUser: true,
+ format: "xdelta3",
+ expectedURL: "anon-delta-url",
+ expectError: false,
+}, {
+ // An error is returned if more than one matching delta is returned by the store,
+ // though this may be handled in the future.
+ info: snap.DownloadInfo{
+ Sha3_384: "sha3",
+ Deltas: []snap.DeltaInfo{
+ {DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 25},
+ {DownloadURL: "bsdiff-delta-url", Format: "xdelta3", FromRevision: 25, ToRevision: 26},
+ },
+ },
+ authenticated: false,
+ format: "xdelta3",
+ expectedURL: "",
+ expectError: true,
+}, {
+ // If the supported format isn't available, an error is returned.
+ info: snap.DownloadInfo{
+ Sha3_384: "sha3",
+ Deltas: []snap.DeltaInfo{
+ {DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
+ {DownloadURL: "ydelta-delta-url", Format: "ydelta", FromRevision: 24, ToRevision: 26},
+ },
+ },
+ authenticated: false,
+ format: "bsdiff",
+ expectedURL: "",
+ expectError: true,
+}}
+
+func (t *remoteRepoTestSuite) TestDownloadDelta(c *C) {
+ origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
+ defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
+ c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
+
+ for _, testCase := range downloadDeltaTests {
+ t.store.deltaFormat = testCase.format
+ download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
+ expectedUser := t.user
+ if testCase.useLocalUser {
+ expectedUser = t.localUser
+ }
+ if !testCase.authenticated {
+ expectedUser = nil
+ }
+ c.Check(user, Equals, expectedUser)
+ c.Check(url, Equals, testCase.expectedURL)
+ w.Write([]byte("I was downloaded"))
+ return nil
+ }
+
+ w, err := ioutil.TempFile("", "")
+ c.Assert(err, IsNil)
+ defer os.Remove(w.Name())
+
+ authedUser := t.user
+ if testCase.useLocalUser {
+ authedUser = t.localUser
+ }
+ if !testCase.authenticated {
+ authedUser = nil
+ }
+
+ err = t.store.downloadDelta("snapname", &testCase.info, w, nil, authedUser)
+
+ if testCase.expectError {
+ c.Assert(err, NotNil)
+ } else {
+ c.Assert(err, IsNil)
+ content, err := ioutil.ReadFile(w.Name())
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "I was downloaded")
+ }
+ }
+}
+
+var applyDeltaTests = []struct {
+ deltaInfo snap.DeltaInfo
+ currentRevision uint
+ error string
+}{{
+ // A supported delta format can be applied.
+ deltaInfo: snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26},
+ currentRevision: 24,
+ error: "",
+}, {
+ // An error is returned if the expected current snap does not exist on disk.
+ deltaInfo: snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26},
+ currentRevision: 23,
+ error: "snap \"foo\" revision 24 not found",
+}, {
+ // An error is returned if the format is not supported.
+ deltaInfo: snap.DeltaInfo{Format: "nodelta", FromRevision: 24, ToRevision: 26},
+ currentRevision: 24,
+ error: "cannot apply unsupported delta format \"nodelta\" (only xdelta3 currently)",
+}}
+
+func (t *remoteRepoTestSuite) TestApplyDelta(c *C) {
+ for _, testCase := range applyDeltaTests {
+ name := "foo"
+ currentSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.currentRevision)
+ currentSnapPath := filepath.Join(dirs.SnapBlobDir, currentSnapName)
+ targetSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.deltaInfo.ToRevision)
+ targetSnapPath := filepath.Join(dirs.SnapBlobDir, targetSnapName)
+ err := os.MkdirAll(filepath.Dir(currentSnapPath), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(currentSnapPath, nil, 0644)
+ c.Assert(err, IsNil)
+ deltaPath := filepath.Join(dirs.SnapBlobDir, "the.delta")
+ err = ioutil.WriteFile(deltaPath, nil, 0644)
+ c.Assert(err, IsNil)
+ // When testing a case where the call to the external
+ // xdelta3 is successful,
+ // simulate the resulting .partial.
+ if testCase.error == "" {
+ err = ioutil.WriteFile(targetSnapPath+".partial", nil, 0644)
+ c.Assert(err, IsNil)
+ }
+
+ err = applyDelta(name, deltaPath, &testCase.deltaInfo, targetSnapPath, "")
+
+ if testCase.error == "" {
+ c.Assert(err, IsNil)
+ c.Assert(t.mockXDelta.Calls(), DeepEquals, [][]string{
+ {"xdelta3", "-d", "-s", currentSnapPath, deltaPath, targetSnapPath + ".partial"},
+ })
+ c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false)
+ c.Assert(osutil.FileExists(targetSnapPath), Equals, true)
+ c.Assert(os.Remove(targetSnapPath), IsNil)
+ } else {
+ c.Assert(err, NotNil)
+ c.Assert(err.Error()[0:len(testCase.error)], Equals, testCase.error)
+ c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false)
+ c.Assert(osutil.FileExists(targetSnapPath), Equals, false)
+ }
+ c.Assert(os.Remove(currentSnapPath), IsNil)
+ c.Assert(os.Remove(deltaPath), IsNil)
+ }
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestSetsAuth(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+ // check user authorization is set
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization is set
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ io.WriteString(w, "response-data")
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestDoesNotSetAuthForLocalOnlyUser(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+ // check no user authorization is set
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, "")
+ // check device authorization is set
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ io.WriteString(w, "response-data")
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.localUser}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.localUser)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestAuthNoSerial(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+ // check user authorization is set
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization was not set
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
+
+ io.WriteString(w, "response-data")
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ // no serial and no device macaroon => no device auth
+ t.device.Serial = ""
+ t.device.SessionMacaroon = ""
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestRefreshesAuth(c *C) {
+ refresh, err := makeTestRefreshDischargeResponse()
+ c.Assert(err, IsNil)
+ c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
+
+ // mock refresh response
+ refreshDischargeEndpointHit := false
+ mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
+ refreshDischargeEndpointHit = true
+ }))
+ defer mockSSOServer.Close()
+ UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
+
+ // mock store response (requiring auth refresh)
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+ if t.user.StoreDischarges[0] == refresh {
+ io.WriteString(w, "response-data")
+ } else {
+ w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
+ w.WriteHeader(http.StatusUnauthorized)
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+ c.Check(refreshDischargeEndpointHit, Equals, true)
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestSetsAndRefreshesDeviceAuth(c *C) {
+ deviceSessionRequested := false
+ refreshSessionRequested := false
+ expiredAuth := `Macaroon root="expired-session-macaroon"`
+ // mock store response
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+
+ switch r.URL.Path {
+ case "/":
+ authorization := r.Header.Get("X-Device-Authorization")
+ if authorization == "" {
+ c.Fatalf("device authentication missing")
+ } else if authorization == expiredAuth {
+ w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
+ w.WriteHeader(http.StatusUnauthorized)
+ } else {
+ c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
+ io.WriteString(w, "response-data")
+ }
+ case "/identity/api/v1/nonces":
+ io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
+ case "/identity/api/v1/sessions":
+ authorization := r.Header.Get("X-Device-Authorization")
+ if authorization == "" {
+ io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
+ deviceSessionRequested = true
+ } else {
+ c.Check(authorization, Equals, expiredAuth)
+ io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
+ refreshSessionRequested = true
+ }
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
+ MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
+
+ // make sure device session is not set
+ t.device.SessionMacaroon = ""
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+ c.Check(deviceSessionRequested, Equals, true)
+ c.Check(refreshSessionRequested, Equals, true)
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestSetsExtraHeaders(c *C) {
+ // Custom headers are applied last.
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, `customAgent`)
+ c.Check(r.Header.Get("X-Foo-Header"), Equals, `Bar`)
+ c.Check(r.Header.Get("Content-Type"), Equals, `application/bson`)
+ c.Check(r.Header.Get("Accept"), Equals, `application/hal+bson`)
+ io.WriteString(w, "response-data")
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ repo := New(&Config{}, nil)
+ c.Assert(repo, NotNil)
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: endpoint,
+ ExtraHeaders: map[string]string{
+ "X-Foo-Header": "Bar",
+ "Content-Type": "application/bson",
+ "Accept": "application/hal+bson",
+ "User-Agent": "customAgent",
+ },
+ }
+
+ response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+}
+
+func (t *remoteRepoTestSuite) TestLoginUser(c *C) {
+ macaroon, err := makeTestMacaroon()
+ c.Assert(err, IsNil)
+ serializedMacaroon, err := auth.MacaroonSerialize(macaroon)
+ c.Assert(err, IsNil)
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, fmt.Sprintf(`{"macaroon": "%s"}`, serializedMacaroon))
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+ MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
+
+ discharge, err := makeTestDischarge()
+ c.Assert(err, IsNil)
+ serializedDischarge, err := auth.MacaroonSerialize(discharge)
+ c.Assert(err, IsNil)
+ mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, serializedDischarge))
+ }))
+ c.Assert(mockSSOServer, NotNil)
+ defer mockSSOServer.Close()
+ UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge"
+
+ userMacaroon, userDischarge, err := LoginUser("username", "password", "otp")
+
+ c.Assert(err, IsNil)
+ c.Check(userMacaroon, Equals, serializedMacaroon)
+ c.Check(userDischarge, Equals, serializedDischarge)
+}
+
+func (t *remoteRepoTestSuite) TestLoginUserMyAppsError(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, "{}")
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+ MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
+
+ userMacaroon, userDischarge, err := LoginUser("username", "password", "otp")
+
+ c.Assert(err, ErrorMatches, "cannot get snap access permission from store: .*")
+ c.Check(userMacaroon, Equals, "")
+ c.Check(userDischarge, Equals, "")
+}
+
+func (t *remoteRepoTestSuite) TestLoginUserSSOError(c *C) {
+ macaroon, err := makeTestMacaroon()
+ c.Assert(err, IsNil)
+ serializedMacaroon, err := auth.MacaroonSerialize(macaroon)
+ c.Assert(err, IsNil)
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, fmt.Sprintf(`{"macaroon": "%s"}`, serializedMacaroon))
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+ MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
+
+ errorResponse := `{"code": "some-error"}`
+ mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(401)
+ io.WriteString(w, errorResponse)
+ }))
+ c.Assert(mockSSOServer, NotNil)
+ defer mockSSOServer.Close()
+ UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge"
+
+ userMacaroon, userDischarge, err := LoginUser("username", "password", "otp")
+
+ c.Assert(err, ErrorMatches, "cannot authenticate to snap store: .*")
+ c.Check(userMacaroon, Equals, "")
+ c.Check(userDischarge, Equals, "")
+}
+
+const (
+ funkyAppName = "8nzc1x4iim2xj1g2ul64"
+ funkyAppDeveloper = "chipaca"
+ funkyAppSnapID = "1e21e12ex4iim2xj1g2ul6f12f1"
+
+ helloWorldSnapID = "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
+ helloWorldDeveloperID = "canonical"
+)
+
+/* acquired via
+
+http --pretty=format --print b https://search.apps.ubuntu.com/api/v1/snaps/details/hello-world X-Ubuntu-Series:16 fields==anon_download_url,architecture,channel,download_sha3_384,summary,description,binary_filesize,download_url,icon_url,last_updated,package_name,prices,publisher,ratings_average,revision,screenshot_urls,snap_id,support_url,title,content,version,origin,developer_id,private,confinement channel==edge | xsel -b
+
+on 2016-07-03. Then, by hand:
+ * set prices to {"EUR": 0.99, "USD": 1.23}.
+ * Screenshot URLS set manually.
+
+On Ubuntu, apt install httpie xsel (although you could get http from
+the http snap instead).
+
+*/
+const MockDetailsJSON = `{
+ "_links": {
+ "curies": [
+ {
+ "href": "https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex#reltype_{rel}",
+ "name": "clickindex",
+ "templated": true
+ }
+ ],
+ "self": {
+ "href": "https://search.apps.ubuntu.com/api/v1/snaps/details/hello-world?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement&channel=edge"
+ }
+ },
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
+ "architecture": [
+ "all"
+ ],
+ "binary_filesize": 20480,
+ "channel": "edge",
+ "confinement": "strict",
+ "content": "application",
+ "description": "This is a simple hello world example.",
+ "developer_id": "canonical",
+ "download_sha3_384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
+ "icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ "last_updated": "2016-07-12T16:37:23.960632Z",
+ "origin": "canonical",
+ "package_name": "hello-world",
+ "prices": {"EUR": 0.99, "USD": 1.23},
+ "publisher": "Canonical",
+ "ratings_average": 0.0,
+ "revision": 27,
+ "screenshot_urls": ["https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png"],
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "summary": "The 'hello-world' of snaps",
+ "support_url": "mailto:snappy-devel@lists.ubuntu.com",
+ "title": "hello-world",
+ "version": "6.3"
+}
+`
+
+const mockOrdersJSON = `{
+ "orders": [
+ {
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "currency": "USD",
+ "amount": "1.99",
+ "state": "Complete",
+ "refundable_until": "2015-07-15 18:46:21",
+ "purchase_date": "2016-09-20T15:00:00+00:00"
+ },
+ {
+ "snap_id": "1e21e12ex4iim2xj1g2ul6f12f1",
+ "currency": "USD",
+ "amount": "1.99",
+ "state": "Complete",
+ "refundable_until": "2015-07-17 11:33:29",
+ "purchase_date": "2016-09-20T15:00:00+00:00"
+ }
+ ]
+}`
+
+const mockOrderResponseJSON = `{
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "currency": "USD",
+ "amount": "1.99",
+ "state": "Complete",
+ "refundable_until": "2015-07-15 18:46:21",
+ "purchase_date": "2016-09-20T15:00:00+00:00"
+}`
+
+const mockSingleOrderJSON = `{
+ "orders": [
+ {
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "currency": "USD",
+ "amount": "1.99",
+ "state": "Complete",
+ "refundable_until": "2015-07-15 18:46:21",
+ "purchase_date": "2016-09-20T15:00:00+00:00"
+ }
+ ]
+}`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ // no store ID by default
+ storeID := r.Header.Get("X-Ubuntu-Store")
+ c.Check(storeID, Equals, "")
+
+ c.Check(r.URL.Path, Equals, "/details/hello-world")
+
+ c.Check(r.URL.Query().Get("channel"), Equals, "edge")
+ c.Check(r.URL.Query().Get("fields"), Equals, "abc,def")
+
+ c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, release.Series)
+ c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "false")
+
+ c.Check(r.Header.Get("X-Ubuntu-Confinement"), Equals, "")
+
+ w.Header().Set("X-Suggested-Currency", "GBP")
+ w.WriteHeader(http.StatusOK)
+
+ io.WriteString(w, MockDetailsJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ DetailFields: []string{"abc", "def"},
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Check(result.Name(), Equals, "hello-world")
+ c.Check(result.Architectures, DeepEquals, []string{"all"})
+ c.Check(result.Revision, Equals, snap.R(27))
+ c.Check(result.SnapID, Equals, helloWorldSnapID)
+ c.Check(result.Publisher, Equals, "canonical")
+ c.Check(result.Version, Equals, "6.3")
+ c.Check(result.Sha3_384, Matches, `[[:xdigit:]]{96}`)
+ c.Check(result.Size, Equals, int64(20480))
+ c.Check(result.Channel, Equals, "edge")
+ c.Check(result.Description(), Equals, "This is a simple hello world example.")
+ c.Check(result.Summary(), Equals, "The 'hello-world' of snaps")
+ c.Assert(result.Prices, DeepEquals, map[string]float64{"EUR": 0.99, "USD": 1.23})
+ c.Assert(result.Screenshots, DeepEquals, []snap.ScreenshotInfo{
+ {
+ URL: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png",
+ },
+ })
+ c.Check(result.MustBuy, Equals, true)
+
+ // Make sure the epoch (currently not sent by the store) defaults to "0"
+ c.Check(result.Epoch, Equals, "0")
+
+ c.Check(repo.SuggestedCurrency(), Equals, "GBP")
+
+ // skip this one until the store supports it
+ // c.Check(result.Private, Equals, true)
+
+ c.Check(snap.Validate(result), IsNil)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails500(c *C) {
+ var n = 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ DetailFields: []string{},
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ _, err = repo.SnapInfo(spec, nil)
+ c.Assert(err, NotNil)
+ c.Assert(err, ErrorMatches, `cannot get details for snap "hello-world" in channel "edge": got unexpected HTTP status code 500 via GET to "http://.*?/details/hello-world\?channel=edge"`)
+ c.Assert(n, Equals, 5)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails500once(c *C) {
+ var n = 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ if n > 1 {
+ w.Header().Set("X-Suggested-Currency", "GBP")
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, MockDetailsJSON)
+ } else {
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Check(result.Name(), Equals, "hello-world")
+ c.Assert(n, Equals, 2)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsAndChannels(c *C) {
+ // this test will break and should be melded into TestUbuntuStoreRepositoryDetails,
+ // above, when the store provides the channels as part of details
+
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, Equals, "/details/hello-world")
+ c.Check(r.URL.Query().Get("channel"), Equals, "")
+ w.Header().Set("X-Suggested-Currency", "GBP")
+ w.WriteHeader(http.StatusOK)
+
+ io.WriteString(w, MockDetailsJSON)
+ case 1:
+ c.Check(r.URL.Path, Equals, "/metadata")
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, `{"_embedded":{"clickindex:package": [
+{"channel": "stable", "confinement": "strict", "revision": 1, "version": "v1"},
+{"channel": "candidate", "confinement": "strict", "revision": 2, "version": "v2"},
+{"channel": "beta", "confinement": "devmode", "revision": 8, "version": "v8"},
+{"channel": "edge", "confinement": "devmode", "revision": 9, "version": "v9"}
+]}}`)
+ default:
+ c.Fatalf("unexpected request to %q", r.URL.Path)
+ }
+ n++
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ bulkURI, err := url.Parse(mockServer.URL + "/metadata")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ BulkURI: bulkURI,
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Assert(n, Equals, 2)
+ c.Check(result.Name(), Equals, "hello-world")
+ c.Check(result.Channels, DeepEquals, map[string]*snap.ChannelSnapInfo{
+ "stable": {
+ Revision: snap.R(1),
+ Version: "v1",
+ Confinement: snap.StrictConfinement,
+ Channel: "stable",
+ },
+ "candidate": {
+ Revision: snap.R(2),
+ Version: "v2",
+ Confinement: snap.StrictConfinement,
+ Channel: "candidate",
+ },
+ "beta": {
+ Revision: snap.R(8),
+ Version: "v8",
+ Confinement: snap.DevModeConfinement,
+ Channel: "beta",
+ },
+ "edge": {
+ Revision: snap.R(9),
+ Version: "v9",
+ Confinement: snap.DevModeConfinement,
+ Channel: "edge",
+ },
+ })
+ c.Check(snap.Validate(result), IsNil)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNonDefaults(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+ os.Setenv("SNAPPY_STORE_NO_CDN", "1")
+ defer os.Unsetenv("SNAPPY_STORE_NO_CDN")
+
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ storeID := r.Header.Get("X-Ubuntu-Store")
+ c.Check(storeID, Equals, "foo")
+
+ c.Check(r.URL.Path, Equals, "/details/hello-world")
+
+ c.Check(r.URL.Query().Get("channel"), Equals, "edge")
+
+ c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, "21")
+ c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, "archXYZ")
+ c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "true")
+ c.Check(r.Header.Get("X-Ubuntu-No-CDN"), Equals, "true")
+
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, MockDetailsJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := DefaultConfig()
+ cfg.DetailsURI = detailsURI
+ cfg.Series = "21"
+ cfg.Architecture = "archXYZ"
+ cfg.StoreID = "foo"
+ repo := New(cfg, nil)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Check(result.Name(), Equals, "hello-world")
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryStoreIDFromAuthContext(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ storeID := r.Header.Get("X-Ubuntu-Store")
+ c.Check(storeID, Equals, "my-brand-store-id")
+
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, MockDetailsJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := DefaultConfig()
+ cfg.DetailsURI = detailsURI
+ cfg.Series = "21"
+ cfg.Architecture = "archXYZ"
+ cfg.StoreID = "fallback"
+ repo := New(cfg, &testAuthContext{c: c, device: t.device, storeID: "my-brand-store-id"})
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Check(result.Name(), Equals, "hello-world")
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRevision(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, ordersPath) {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ c.Check(r.URL.Path, Equals, "/details/hello-world")
+ c.Check(r.URL.Query(), DeepEquals, url.Values{
+ "channel": []string{""},
+ "revision": []string{"26"},
+ })
+
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, MockDetailsJSON)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := DefaultConfig()
+ cfg.DetailsURI = detailsURI
+ cfg.OrdersURI = ordersURI
+ cfg.DetailFields = []string{}
+ repo := New(cfg, nil)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(26),
+ }
+ result, err := repo.SnapInfo(spec, t.user)
+ c.Assert(err, IsNil)
+ c.Check(result.Name(), Equals, "hello-world")
+ c.Check(result.Revision, DeepEquals, snap.R(27))
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsOopses(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, Equals, "/details/hello-world")
+ c.Check(r.URL.Query().Get("channel"), Equals, "edge")
+
+ w.Header().Set("X-Oops-Id", "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f")
+ w.WriteHeader(http.StatusInternalServerError)
+
+ io.WriteString(w, `{"oops": "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f"}`)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ _, err = repo.SnapInfo(spec, nil)
+ c.Assert(err, ErrorMatches, `cannot get details for snap "hello-world" in channel "edge": got unexpected HTTP status code 5.. via GET to "http://\S+" \[OOPS-[[:xdigit:]]*\]`)
+}
+
+/*
+acquired via
+
+http --pretty=format --print b https://search.apps.ubuntu.com/api/v1/snaps/details/no:such:package X-Ubuntu-Series:16 fields==anon_download_url,architecture,channel,download_sha512,summary,description,binary_filesize,download_url,icon_url,last_updated,package_name,prices,publisher,ratings_average,revision,snap_id,support_url,title,content,version,origin,developer_id,private,confinement channel==edge | xsel -b
+
+on 2016-07-03
+
+On Ubuntu, apt install httpie xsel (although you could get http from
+the http snap instead).
+
+*/
+const MockNoDetailsJSON = `{
+ "errors": [
+ "No such package"
+ ],
+ "result": "error"
+}`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNoDetails(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, Equals, "/details/no-such-pkg")
+
+ q := r.URL.Query()
+ c.Check(q.Get("channel"), Equals, "edge")
+ w.WriteHeader(404)
+ io.WriteString(w, MockNoDetailsJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ // the actual test
+ spec := SnapSpec{
+ Name: "no-such-pkg",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, NotNil)
+ c.Assert(result, IsNil)
+}
+
+func (t *remoteRepoTestSuite) TestStructFields(c *C) {
+ type s struct {
+ Foo int `json:"hello"`
+ Bar int `json:"potato,stuff"`
+ }
+ c.Assert(getStructFields(s{}), DeepEquals, []string{"hello", "potato"})
+}
+
+/* acquired via:
+curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://search.apps.ubuntu.com/api/v1/search?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&q=hello' | python -m json.tool | xsel -b
+Screenshot URLS set manually.
+*/
+const MockSearchJSON = `{
+ "_embedded": {
+ "clickindex:package": [
+ {
+ "_links": {
+ "self": {
+ "href": "https://search.apps.ubuntu.com/api/v1/package/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
+ }
+ },
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25.snap",
+ "architecture": [
+ "all"
+ ],
+ "binary_filesize": 20480,
+ "channel": "edge",
+ "content": "application",
+ "description": "This is a simple hello world example.",
+ "download_sha512": "4bf23ce93efa1f32f0aeae7ec92564b7b0f9f8253a0bd39b2741219c1be119bb676c21208c6845ccf995e6aabe791d3f28a733ebcbbc3171bb23f67981f4068e",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25.snap",
+ "icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ "last_updated": "2016-04-19T19:50:50.435291Z",
+ "origin": "canonical",
+ "package_name": "hello-world",
+ "prices": {"EUR": 2.99, "USD": 3.49},
+ "publisher": "Canonical",
+ "ratings_average": 0.0,
+ "revision": 25,
+ "screenshot_urls": ["https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png"],
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "summary": "Hello world example",
+ "support_url": "mailto:snappy-devel@lists.ubuntu.com",
+ "title": "hello-world",
+ "version": "6.0"
+ }
+ ]
+ },
+ "_links": {
+ "curies": [
+ {
+ "href": "https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex#reltype_{rel}",
+ "name": "clickindex",
+ "templated": true
+ }
+ ],
+ "first": {
+ "href": "https://search.apps.ubuntu.com/api/v1/search?q=hello&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&page=1"
+ },
+ "last": {
+ "href": "https://search.apps.ubuntu.com/api/v1/search?q=hello&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&page=1"
+ },
+ "self": {
+ "href": "https://search.apps.ubuntu.com/api/v1/search?q=hello&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&page=1"
+ }
+ }
+}
+`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindQueries(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ query := r.URL.Query()
+
+ name := query.Get("name")
+ q := query.Get("q")
+ section := query.Get("section")
+
+ c.Check(r.URL.Path, Equals, "/search")
+ c.Check(query.Get("fields"), Equals, "abc,def")
+
+ // write dummy json so that Find doesn't re-try due to json decoder EOF error
+ io.WriteString(w, "{}")
+
+ switch n {
+ case 0:
+ c.Check(name, Equals, "hello")
+ c.Check(q, Equals, "")
+ c.Check(section, Equals, "")
+ case 1:
+ c.Check(name, Equals, "")
+ c.Check(q, Equals, "hello")
+ c.Check(section, Equals, "")
+ case 2:
+ c.Check(name, Equals, "")
+ c.Check(q, Equals, "")
+ c.Check(section, Equals, "db")
+ case 3:
+ c.Check(name, Equals, "")
+ c.Check(q, Equals, "hello")
+ c.Check(section, Equals, "db")
+ default:
+ c.Fatalf("what? %d", n)
+ }
+
+ n++
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ serverURL, _ := url.Parse(mockServer.URL)
+ searchURI, _ := serverURL.Parse("/search")
+ detailsURI, _ := serverURL.Parse("/details/")
+ cfg := Config{
+ DetailsURI: detailsURI,
+ SearchURI: searchURI,
+ DetailFields: []string{"abc", "def"},
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ for _, query := range []Search{
+ {Query: "hello", Prefix: true},
+ {Query: "hello"},
+ {Section: "db"},
+ {Query: "hello", Section: "db"},
+ } {
+ repo.Find(&query, nil)
+ }
+}
+
+/* acquired via:
+curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://search.apps.ubuntu.com/api/v1/snaps/sections'
+*/
+const MockSectionsJSON = `{
+ "_embedded": {
+ "clickindex:sections": [
+ {
+ "name": "featured"
+ },
+ {
+ "name": "database"
+ }
+ ]
+ },
+ "_links": {
+ "curies": [
+ {
+ "href": "https://search.apps.ubuntu.com/docs/#reltype-{rel}",
+ "name": "clickindex",
+ "templated": true,
+ "type": "text/html"
+ }
+ ],
+ "self": {
+ "href": "http://search.apps.ubuntu.com/api/v1/snaps/sections"
+ }
+ }
+}
+`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreSectionsQuery(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, Equals, "/snaps/sections")
+ default:
+ c.Fatalf("what? %d", n)
+ }
+
+ w.Header().Set("Content-Type", "application/hal+json")
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, MockSectionsJSON)
+ n++
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ serverURL, _ := url.Parse(mockServer.URL)
+ searchSectionsURI, _ := serverURL.Parse("/snaps/sections")
+ cfg := Config{
+ SectionsURI: searchSectionsURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ sections, err := repo.Sections(t.user)
+ c.Check(err, IsNil)
+ c.Check(sections, DeepEquals, []string{"featured", "database"})
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindPrivate(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query()
+
+ name := query.Get("name")
+ q := query.Get("q")
+
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, Equals, "/search")
+ c.Check(name, Equals, "")
+ c.Check(q, Equals, "foo")
+ c.Check(query.Get("private"), Equals, "true")
+ default:
+ c.Fatalf("what? %d", n)
+ }
+
+ w.Header().Set("Content-Type", "application/hal+json")
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1))
+
+ n++
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ serverURL, _ := url.Parse(mockServer.URL)
+ searchURI, _ := serverURL.Parse("/search")
+ cfg := Config{
+ SearchURI: searchURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ _, err := repo.Find(&Search{Query: "foo", Private: true}, t.user)
+ c.Check(err, IsNil)
+
+ _, err = repo.Find(&Search{Query: "foo", Private: true}, nil)
+ c.Check(err, Equals, ErrUnauthenticated)
+
+ _, err = repo.Find(&Search{Query: "name:foo", Private: true}, t.user)
+ c.Check(err, Equals, ErrBadQuery)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindFailures(c *C) {
+ repo := New(&Config{SearchURI: new(url.URL)}, nil)
+ _, err := repo.Find(&Search{Query: "foo:bar"}, nil)
+ c.Check(err, Equals, ErrBadQuery)
+ _, err = repo.Find(&Search{Query: "foo", Private: true, Prefix: true}, t.user)
+ c.Check(err, Equals, ErrBadQuery)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindFails(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Query().Get("q"), Equals, "hello")
+ http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ searchURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ SearchURI: searchURI,
+ DetailFields: []string{}, // make the error less noisy
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ snaps, err := repo.Find(&Search{Query: "hello"}, nil)
+ c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 418 via GET to "http://\S+[?&]q=hello.*"`)
+ c.Check(snaps, HasLen, 0)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadContentType(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Query().Get("q"), Equals, "hello")
+ io.WriteString(w, MockSearchJSON)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ searchURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ SearchURI: searchURI,
+ DetailFields: []string{}, // make the error less noisy
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ snaps, err := repo.Find(&Search{Query: "hello"}, nil)
+ c.Check(err, ErrorMatches, `received an unexpected content type \("text/plain[^"]+"\) when trying to search via "http://\S+[?&]q=hello.*"`)
+ c.Check(snaps, HasLen, 0)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadBody(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query()
+ c.Check(query.Get("q"), Equals, "hello")
+ w.Header().Set("Content-Type", "application/hal+json")
+ io.WriteString(w, "<hello>")
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ searchURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ SearchURI: searchURI,
+ DetailFields: []string{}, // make the error less noisy
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ snaps, err := repo.Find(&Search{Query: "hello"}, nil)
+ c.Check(err, ErrorMatches, `invalid character '<' looking for beginning of value`)
+ c.Check(snaps, HasLen, 0)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFind500(c *C) {
+ var n = 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ searchURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ SearchURI: searchURI,
+ DetailFields: []string{},
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ _, err = repo.Find(&Search{Query: "hello"}, nil)
+ c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 500 via GET to "http://\S+[?&]q=hello.*"`)
+ c.Assert(n, Equals, 5)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFind500once(c *C) {
+ var n = 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ if n == 1 {
+ w.WriteHeader(http.StatusInternalServerError)
+ } else {
+ w.Header().Set("Content-Type", "application/hal+json")
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1))
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ searchURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ SearchURI: searchURI,
+ DetailFields: []string{},
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ snaps, err := repo.Find(&Search{Query: "hello"}, nil)
+ c.Check(err, IsNil)
+ c.Assert(snaps, HasLen, 1)
+ c.Assert(n, Equals, 2)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreFindAuthFailed(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check authorization is set
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+
+ query := r.URL.Query()
+ c.Check(query.Get("q"), Equals, "foo")
+ if release.OnClassic {
+ c.Check(query.Get("confinement"), Matches, `strict,classic|classic,strict`)
+ } else {
+ c.Check(query.Get("confinement"), Equals, "strict")
+ }
+ w.Header().Set("Content-Type", "application/hal+json")
+ io.WriteString(w, MockSearchJSON)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, ordersPath)
+ w.WriteHeader(http.StatusUnauthorized)
+ io.WriteString(w, "{}")
+ }))
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ var err error
+ searchURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ SearchURI: searchURI,
+ OrdersURI: ordersURI,
+ DetailFields: []string{}, // make the error less noisy
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ snaps, err := repo.Find(&Search{Query: "foo"}, t.user)
+ c.Assert(err, IsNil)
+
+ // Check that we log an error.
+ c.Check(t.logbuf.String(), Matches, "(?ms).* cannot get user orders: invalid credentials")
+
+ // But still successfully return snap information.
+ c.Assert(snaps, HasLen, 1)
+ c.Check(snaps[0].SnapID, Equals, helloWorldSnapID)
+ c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49})
+ c.Check(snaps[0].MustBuy, Equals, true)
+}
+
+/* acquired via:
+(against production "hello-world")
+$ curl -s --data-binary '{"snaps":[{"snap_id":"buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ","channel":"stable","revision":25,"epoch":"0","confinement":"strict"}],"fields":["anon_download_url","architecture","channel","download_sha512","summary","description","binary_filesize","download_url","icon_url","last_updated","package_name","prices","publisher","ratings_average","revision","snap_id","support_url","title","content","version","origin","developer_id","private","confinement"]}' -H 'content-type: application/json' -H 'X-Ubuntu-Release: 16' -H 'X-Ubuntu-Wire-Protocol: 1' -H "accept: application/hal+json" https://search.apps.ubuntu.com/api/v1/snaps/metadata | python3 -m json.tool --sort-keys | xsel -b
+*/
+var MockUpdatesJSON = `
+{
+ "_embedded": {
+ "clickindex:package": [
+ {
+ "_links": {
+ "self": {
+ "href": "https://search.apps.ubuntu.com/api/v1/package/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
+ }
+ },
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
+ "architecture": [
+ "all"
+ ],
+ "binary_filesize": 20480,
+ "channel": "stable",
+ "confinement": "strict",
+ "content": "application",
+ "description": "This is a simple hello world example.",
+ "developer_id": "canonical",
+ "download_sha512": "345f33c06373f799b64c497a778ef58931810dd7ae85279d6917d8b4f43d38abaf37e68239cb85914db276cb566a0ef83ea02b6f2fd064b54f9f2508fa4ca1f1",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
+ "icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ "last_updated": "2016-05-31T07:02:32.586839Z",
+ "origin": "canonical",
+ "package_name": "hello-world",
+ "prices": {},
+ "publisher": "Canonical",
+ "ratings_average": 0.0,
+ "revision": 26,
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "summary": "Hello world example",
+ "support_url": "mailto:snappy-devel@lists.ubuntu.com",
+ "title": "hello-world",
+ "version": "6.1"
+ }
+ ]
+ },
+ "_links": {
+ "curies": [
+ {
+ "href": "https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex#reltype_{rel}",
+ "name": "clickindex",
+ "templated": true
+ }
+ ]
+ }
+}
+`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefresh(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ var resp struct {
+ Snaps []map[string]interface{} `json:"snaps"`
+ Fields []string `json:"fields"`
+ }
+
+ err = json.Unmarshal(jsonReq, &resp)
+ c.Assert(err, IsNil)
+
+ c.Assert(resp.Snaps, HasLen, 1)
+ c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
+ "snap_id": helloWorldSnapID,
+ "channel": "stable",
+ "revision": float64(1),
+ "epoch": "0",
+ "confinement": "",
+ })
+ c.Assert(resp.Fields, DeepEquals, detailFields)
+
+ io.WriteString(w, MockUpdatesJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ results, err := repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(1),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, IsNil)
+ c.Assert(results, HasLen, 1)
+ c.Assert(results[0].Name(), Equals, "hello-world")
+ c.Assert(results[0].Revision, Equals, snap.R(26))
+ c.Assert(results[0].Version, Equals, "6.1")
+ c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
+ c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID)
+ c.Assert(results[0].Deltas, HasLen, 0)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshUnauthorised(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ w.WriteHeader(http.StatusUnauthorized)
+ io.WriteString(w, "")
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ _, err = repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(24),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(n, Equals, 1)
+ c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 401 via POST to "http://.*?/updates/"`)
+}
+func (t *remoteRepoTestSuite) TestListRefresh500(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ _, err = repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(24),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://.*?/updates/"`)
+ c.Assert(n, Equals, 5)
+}
+
+func (t *remoteRepoTestSuite) TestListRefresh500DurationExceeded(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ time.Sleep(time.Duration(2) * time.Second)
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ _, err = repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(24),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://.*?/updates/"`)
+ c.Assert(n, Equals, 1)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipCurrent(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ var resp struct {
+ Snaps []map[string]interface{} `json:"snaps"`
+ }
+
+ err = json.Unmarshal(jsonReq, &resp)
+ c.Assert(err, IsNil)
+
+ c.Assert(resp.Snaps, HasLen, 1)
+ c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
+ "snap_id": helloWorldSnapID,
+ "channel": "stable",
+ "revision": float64(26),
+ "epoch": "0",
+ "confinement": "",
+ })
+
+ io.WriteString(w, MockUpdatesJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ results, err := repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(26),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, IsNil)
+ c.Assert(results, HasLen, 0)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipBlocked(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+
+ var resp struct {
+ Snaps []map[string]interface{} `json:"snaps"`
+ }
+
+ err = json.Unmarshal(jsonReq, &resp)
+ c.Assert(err, IsNil)
+
+ c.Assert(resp.Snaps, HasLen, 1)
+ c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
+ "snap_id": helloWorldSnapID,
+ "channel": "stable",
+ "revision": float64(25),
+ "epoch": "0",
+ "confinement": "",
+ })
+
+ io.WriteString(w, MockUpdatesJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ results, err := repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(25),
+ Epoch: "0",
+ Block: []snap.Revision{snap.R(26)},
+ },
+ }, nil)
+ c.Assert(err, IsNil)
+ c.Assert(results, HasLen, 0)
+}
+
+/* XXX Currently this is just MockUpdatesJSON with the deltas that we're
+planning to add to the stores /api/v1/snaps/metadata response.
+*/
+var MockUpdatesWithDeltasJSON = `
+{
+ "_embedded": {
+ "clickindex:package": [
+ {
+ "_links": {
+ "self": {
+ "href": "https://search.apps.ubuntu.com/api/v1/package/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
+ }
+ },
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
+ "architecture": [
+ "all"
+ ],
+ "binary_filesize": 20480,
+ "channel": "stable",
+ "confinement": "strict",
+ "content": "application",
+ "description": "This is a simple hello world example.",
+ "developer_id": "canonical",
+ "download_sha512": "345f33c06373f799b64c497a778ef58931810dd7ae85279d6917d8b4f43d38abaf37e68239cb85914db276cb566a0ef83ea02b6f2fd064b54f9f2508fa4ca1f1",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
+ "icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ "last_updated": "2016-05-31T07:02:32.586839Z",
+ "origin": "canonical",
+ "package_name": "hello-world",
+ "prices": {},
+ "publisher": "Canonical",
+ "ratings_average": 0.0,
+ "revision": 26,
+ "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
+ "summary": "Hello world example",
+ "support_url": "mailto:snappy-devel@lists.ubuntu.com",
+ "title": "hello-world",
+ "version": "6.1",
+ "deltas": [{
+ "from_revision": 24,
+ "to_revision": 25,
+ "format": "xdelta3",
+ "binary_filesize": 204,
+ "download_sha3_384": "sha3_384_hash",
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta"
+ }, {
+ "from_revision": 25,
+ "to_revision": 26,
+ "format": "xdelta3",
+ "binary_filesize": 206,
+ "download_sha3_384": "sha3_384_hash",
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta"
+ }]
+ }
+ ]
+ },
+ "_links": {
+ "curies": [
+ {
+ "href": "https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex#reltype_{rel}",
+ "name": "clickindex",
+ "templated": true
+ }
+ ]
+ }
+}
+`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithDeltas(c *C) {
+ origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
+ defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
+ c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
+
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("X-Ubuntu-Delta-Formats"), Equals, `xdelta3`)
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ var resp struct {
+ Snaps []map[string]interface{} `json:"snaps"`
+ Fields []string `json:"fields"`
+ }
+
+ err = json.Unmarshal(jsonReq, &resp)
+ c.Assert(err, IsNil)
+
+ c.Assert(resp.Snaps, HasLen, 1)
+ c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
+ "snap_id": helloWorldSnapID,
+ "channel": "stable",
+ "revision": float64(24),
+ "epoch": "0",
+ "confinement": "",
+ })
+ c.Assert(resp.Fields, DeepEquals, getStructFields(snapDetails{}))
+
+ io.WriteString(w, MockUpdatesWithDeltasJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ results, err := repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(24),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, IsNil)
+ c.Assert(results, HasLen, 1)
+ c.Assert(results[0].Deltas, HasLen, 2)
+ c.Assert(results[0].Deltas[0], Equals, snap.DeltaInfo{
+ FromRevision: 24,
+ ToRevision: 25,
+ Format: "xdelta3",
+ AnonDownloadURL: "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta",
+ DownloadURL: "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta",
+ Size: 204,
+ Sha3_384: "sha3_384_hash",
+ })
+ c.Assert(results[0].Deltas[1], Equals, snap.DeltaInfo{
+ FromRevision: 25,
+ ToRevision: 26,
+ Format: "xdelta3",
+ AnonDownloadURL: "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta",
+ DownloadURL: "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta",
+ Size: 206,
+ Sha3_384: "sha3_384_hash",
+ })
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithoutDeltas(c *C) {
+ // Verify the X-Delta-Format header is not set.
+ origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
+ defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
+ c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "0"), IsNil)
+
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("X-Ubuntu-Delta-Formats"), Equals, ``)
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ var resp struct {
+ Snaps []map[string]interface{} `json:"snaps"`
+ Fields []string `json:"fields"`
+ }
+
+ err = json.Unmarshal(jsonReq, &resp)
+ c.Assert(err, IsNil)
+
+ c.Assert(resp.Snaps, HasLen, 1)
+ c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
+ "snap_id": helloWorldSnapID,
+ "channel": "stable",
+ "revision": float64(24),
+ "epoch": "0",
+ "confinement": "",
+ })
+ c.Assert(resp.Fields, DeepEquals, detailFields)
+
+ io.WriteString(w, MockUpdatesJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ results, err := repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(24),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, IsNil)
+ c.Assert(results, HasLen, 1)
+ c.Assert(results[0].Deltas, HasLen, 0)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdateNotSendLocalRevs(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ var resp struct {
+ Snaps []map[string]interface{} `json:"snaps"`
+ }
+
+ err = json.Unmarshal(jsonReq, &resp)
+ c.Assert(err, IsNil)
+
+ c.Assert(resp.Snaps, HasLen, 1)
+ c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
+ "snap_id": helloWorldSnapID,
+ "channel": "stable",
+ "epoch": "0",
+ "confinement": "",
+ })
+
+ io.WriteString(w, MockUpdatesJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ bulkURI, err := url.Parse(mockServer.URL + "/updates/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ BulkURI: bulkURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ _, err = repo.ListRefresh([]*RefreshCandidate{
+ {
+ SnapID: helloWorldSnapID,
+ Channel: "stable",
+ Revision: snap.R(-2),
+ Epoch: "0",
+ },
+ }, nil)
+ c.Assert(err, IsNil)
+}
+
+func (t *remoteRepoTestSuite) TestStructFieldsSurvivesNoTag(c *C) {
+ type s struct {
+ Foo int `json:"hello"`
+ Bar int
+ }
+ c.Assert(getStructFields(s{}), DeepEquals, []string{"hello"})
+}
+
+func (t *remoteRepoTestSuite) TestCpiURLDependsOnEnviron(c *C) {
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
+ before := cpiURL()
+
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
+ defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
+ after := cpiURL()
+
+ c.Check(before, Not(Equals), after)
+}
+
+func (t *remoteRepoTestSuite) TestAuthLocationDependsOnEnviron(c *C) {
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
+ before := authLocation()
+
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
+ defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
+ after := authLocation()
+
+ c.Check(before, Not(Equals), after)
+}
+
+func (t *remoteRepoTestSuite) TestAuthURLDependsOnEnviron(c *C) {
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
+ before := authURL()
+
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
+ defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
+ after := authURL()
+
+ c.Check(before, Not(Equals), after)
+}
+
+func (t *remoteRepoTestSuite) TestAssertsURLDependsOnEnviron(c *C) {
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
+ before := assertsURL()
+
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
+ defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
+ after := assertsURL()
+
+ c.Check(before, Not(Equals), after)
+}
+
+func (t *remoteRepoTestSuite) TestMyAppsURLDependsOnEnviron(c *C) {
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
+ before := myappsURL()
+
+ c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
+ defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
+ after := myappsURL()
+
+ c.Check(before, Not(Equals), after)
+}
+
+func (t *remoteRepoTestSuite) TestDefaultConfig(c *C) {
+ c.Check(strings.HasPrefix(defaultConfig.SearchURI.String(), "https://search.apps.ubuntu.com/api/v1/snaps/search"), Equals, true)
+ c.Check(strings.HasPrefix(defaultConfig.BulkURI.String(), "https://search.apps.ubuntu.com/api/v1/snaps/metadata"), Equals, true)
+ c.Check(defaultConfig.AssertionsURI.String(), Equals, "https://assertions.ubuntu.com/v1/assertions/")
+}
+
+func (t *remoteRepoTestSuite) TestNew(c *C) {
+ aStore := New(nil, nil)
+ fields := strings.Join(detailFields, ",")
+ // check for fields
+ c.Check(aStore.detailFields, DeepEquals, detailFields)
+ c.Check(aStore.searchURI.Query().Get("fields"), Equals, fields)
+ c.Check(aStore.detailsURI.Query().Get("fields"), Equals, fields)
+ c.Check(aStore.bulkURI.Query(), DeepEquals, url.Values{})
+ c.Check(aStore.sectionsURI.Query(), DeepEquals, url.Values{})
+ c.Check(aStore.assertionsURI.Query(), DeepEquals, url.Values{})
+}
+
+var testAssertion = `type: snap-declaration
+authority-id: super
+series: 16
+snap-id: snapidfoo
+publisher-id: devidbaz
+snap-name: mysnap
+timestamp: 2016-03-30T12:22:16Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+openpgp wsBcBAABCAAQBQJW+8VBCRDWhXkqAWcrfgAAQ9gIABZFgMPByJZeUE835FkX3/y2hORn
+AzE3R1ktDkQEVe/nfVDMACAuaw1fKmUS4zQ7LIrx/AZYw5i0vKVmJszL42LBWVsqR0+p9Cxebzv9
+U2VUSIajEsUUKkBwzD8wxFzagepFlScif1NvCGZx0vcGUOu0Ent0v+gqgAv21of4efKqEW7crlI1
+T/A8LqZYmIzKRHGwCVucCyAUD8xnwt9nyWLgLB+LLPOVFNK8SR6YyNsX05Yz1BUSndBfaTN8j/k8
+8isKGZE6P0O9ozBbNIAE8v8NMWQegJ4uWuil7D3psLkzQIrxSypk9TrQ2GlIG2hJdUovc5zBuroe
+xS4u9rVT6UY=`
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion(c *C) {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 88)
+ defer restore()
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
+ c.Check(r.URL.Path, Equals, "/assertions/snap-declaration/16/snapidfoo")
+ c.Check(r.URL.Query().Get("max-format"), Equals, "88")
+ io.WriteString(w, testAssertion)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ assertionsURI, err := url.Parse(mockServer.URL + "/assertions/")
+ c.Assert(err, IsNil)
+
+ cfg := Config{
+ AssertionsURI: assertionsURI,
+ }
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
+
+ a, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
+ c.Assert(err, IsNil)
+ c.Check(a, NotNil)
+ c.Check(a.Type(), Equals, asserts.SnapDeclarationType)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionNotFound(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
+ c.Check(r.URL.Path, Equals, "/assertions/snap-declaration/16/snapidfoo")
+ w.Header().Set("Content-Type", "application/problem+json")
+ w.WriteHeader(404)
+ io.WriteString(w, `{"status": 404,"title": "not found"}`)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ assertionsURI, err := url.Parse(mockServer.URL + "/assertions/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ AssertionsURI: assertionsURI,
+ }
+ repo := New(&cfg, nil)
+
+ _, err = repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
+ c.Check(err, DeepEquals, &AssertionNotFoundError{
+ Ref: &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snapidfoo"},
+ },
+ })
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion500(c *C) {
+ var n = 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ var err error
+ assertionsURI, err := url.Parse(mockServer.URL + "/assertions/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ AssertionsURI: assertionsURI,
+ }
+ repo := New(&cfg, nil)
+
+ _, err = repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
+ c.Assert(err, ErrorMatches, `cannot fetch assertion: got unexpected HTTP status code 500 via .+`)
+ c.Assert(n, Equals, 5)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) {
+ suggestedCurrency := "GBP"
+
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Suggested-Currency", suggestedCurrency)
+ w.WriteHeader(http.StatusOK)
+
+ io.WriteString(w, MockDetailsJSON)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL + "/details/")
+ c.Assert(err, IsNil)
+ cfg := Config{
+ DetailsURI: detailsURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ // the store doesn't know the currency until after the first search, so fall back to dollars
+ c.Check(repo.SuggestedCurrency(), Equals, "USD")
+
+ // we should soon have a suggested currency
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ result, err := repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Assert(result, NotNil)
+ c.Check(repo.SuggestedCurrency(), Equals, "GBP")
+
+ suggestedCurrency = "EUR"
+
+ // checking the currency updates
+ result, err = repo.SnapInfo(spec, nil)
+ c.Assert(err, IsNil)
+ c.Assert(result, NotNil)
+ c.Check(repo.SuggestedCurrency(), Equals, "EUR")
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrders(c *C) {
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.URL.Path, Equals, ordersPath)
+ io.WriteString(w, mockOrdersJSON)
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ var err error
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+ helloWorld.Prices = map[string]float64{"USD": 1.23}
+
+ funkyApp := &snap.Info{}
+ funkyApp.SnapID = funkyAppSnapID
+ funkyApp.Prices = map[string]float64{"USD": 2.34}
+
+ otherApp := &snap.Info{}
+ otherApp.SnapID = "other"
+ otherApp.Prices = map[string]float64{"USD": 3.45}
+
+ otherApp2 := &snap.Info{}
+ otherApp2.SnapID = "other2"
+
+ snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
+
+ err = repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, IsNil)
+
+ c.Check(helloWorld.MustBuy, Equals, false)
+ c.Check(funkyApp.MustBuy, Equals, false)
+ c.Check(otherApp.MustBuy, Equals, true)
+ c.Check(otherApp2.MustBuy, Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersFailedAccess(c *C) {
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, ordersPath)
+ w.WriteHeader(http.StatusUnauthorized)
+ io.WriteString(w, "{}")
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ var err error
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+ helloWorld.Prices = map[string]float64{"USD": 1.23}
+
+ funkyApp := &snap.Info{}
+ funkyApp.SnapID = funkyAppSnapID
+ funkyApp.Prices = map[string]float64{"USD": 2.34}
+
+ otherApp := &snap.Info{}
+ otherApp.SnapID = "other"
+ otherApp.Prices = map[string]float64{"USD": 3.45}
+
+ otherApp2 := &snap.Info{}
+ otherApp2.SnapID = "other2"
+
+ snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
+
+ err = repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, NotNil)
+
+ c.Check(helloWorld.MustBuy, Equals, true)
+ c.Check(funkyApp.MustBuy, Equals, true)
+ c.Check(otherApp.MustBuy, Equals, true)
+ c.Check(otherApp2.MustBuy, Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersNoAuth(c *C) {
+ cfg := Config{}
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+ helloWorld.Prices = map[string]float64{"USD": 1.23}
+
+ funkyApp := &snap.Info{}
+ funkyApp.SnapID = funkyAppSnapID
+ funkyApp.Prices = map[string]float64{"USD": 2.34}
+
+ otherApp := &snap.Info{}
+ otherApp.SnapID = "other"
+ otherApp.Prices = map[string]float64{"USD": 3.45}
+
+ otherApp2 := &snap.Info{}
+ otherApp2.SnapID = "other2"
+
+ snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
+
+ err := repo.decorateOrders(snaps, "edge", nil)
+ c.Assert(err, IsNil)
+
+ c.Check(helloWorld.MustBuy, Equals, true)
+ c.Check(funkyApp.MustBuy, Equals, true)
+ c.Check(otherApp.MustBuy, Equals, true)
+ c.Check(otherApp2.MustBuy, Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersAllFree(c *C) {
+ requestRecieved := false
+
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ requestRecieved = true
+ io.WriteString(w, `{"orders": []}`)
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+ cfg := Config{
+ OrdersURI: ordersURI,
+ }
+
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ // This snap is free
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+
+ // This snap is also free
+ funkyApp := &snap.Info{}
+ funkyApp.SnapID = funkyAppSnapID
+
+ snaps := []*snap.Info{helloWorld, funkyApp}
+
+ // There should be no request to the purchase server.
+ err = repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, IsNil)
+ c.Check(requestRecieved, Equals, false)
+}
+
+const ordersPath = "/purchases/v1/orders"
+const customersMePath = "/purchases/v1/customers/me"
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingle(c *C) {
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, ordersPath)
+ io.WriteString(w, mockSingleOrderJSON)
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ var err error
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+ helloWorld.Prices = map[string]float64{"USD": 1.23}
+
+ snaps := []*snap.Info{helloWorld}
+
+ err = repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, IsNil)
+ c.Check(helloWorld.MustBuy, Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingleFreeSnap(c *C) {
+ cfg := Config{}
+ repo := New(&cfg, nil)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+
+ snaps := []*snap.Info{helloWorld}
+
+ err := repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, IsNil)
+ c.Check(helloWorld.MustBuy, Equals, false)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingleNotFound(c *C) {
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, ordersPath)
+ w.WriteHeader(http.StatusNotFound)
+ io.WriteString(w, "{}")
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ var err error
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+ helloWorld.Prices = map[string]float64{"USD": 1.23}
+
+ snaps := []*snap.Info{helloWorld}
+
+ err = repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, NotNil)
+ c.Check(helloWorld.MustBuy, Equals, true)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersTokenExpired(c *C) {
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, ordersPath)
+ w.WriteHeader(http.StatusUnauthorized)
+ io.WriteString(w, "")
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ var err error
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ helloWorld := &snap.Info{}
+ helloWorld.SnapID = helloWorldSnapID
+ helloWorld.Prices = map[string]float64{"USD": 1.23}
+
+ snaps := []*snap.Info{helloWorld}
+
+ err = repo.decorateOrders(snaps, "edge", t.user)
+ c.Assert(err, NotNil)
+ c.Check(helloWorld.MustBuy, Equals, true)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreMustBuy(c *C) {
+ free := map[string]float64{}
+ priced := map[string]float64{"USD": 2.99}
+
+ // Never need to buy a free snap.
+ c.Check(mustBuy(free, true), Equals, false)
+ c.Check(mustBuy(free, false), Equals, false)
+
+ // Don't need to buy snaps that have been bought.
+ c.Check(mustBuy(priced, true), Equals, false)
+
+ // Need to buy snaps that aren't bought.
+ c.Check(mustBuy(priced, false), Equals, true)
+}
+
+const customersMeValid = `
+{
+ "latest_tos_date": "2016-09-14T00:00:00+00:00",
+ "accepted_tos_date": "2016-09-14T15:56:49+00:00",
+ "latest_tos_accepted": true,
+ "has_payment_method": true
+}
+`
+
+var buyTests = []struct {
+ suggestedCurrency string
+ expectedInput string
+ buyStatus int
+ buyResponse string
+ buyErrorMessage string
+ buyErrorCode string
+ snapID string
+ price float64
+ currency string
+ expectedResult *BuyResult
+ expectedError string
+}{
+ {
+ // successful buying
+ suggestedCurrency: "EUR",
+ expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"0.99","currency":"EUR"}`,
+ buyResponse: mockOrderResponseJSON,
+ expectedResult: &BuyResult{State: "Complete"},
+ },
+ {
+ // failure due to invalid price
+ suggestedCurrency: "USD",
+ expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"5.99","currency":"USD"}`,
+ buyStatus: http.StatusBadRequest,
+ buyErrorCode: "invalid-field",
+ buyErrorMessage: "invalid price specified",
+ price: 5.99,
+ expectedError: "cannot buy snap: bad request: store reported an error: invalid price specified",
+ },
+ {
+ // failure due to unknown snap ID
+ suggestedCurrency: "USD",
+ expectedInput: `{"snap_id":"invalid snap ID","amount":"0.99","currency":"EUR"}`,
+ buyStatus: http.StatusNotFound,
+ buyErrorCode: "not-found",
+ buyErrorMessage: "Not found",
+ snapID: "invalid snap ID",
+ price: 0.99,
+ currency: "EUR",
+ expectedError: "cannot buy snap: server says not found (snap got removed?)",
+ },
+ {
+ // failure due to "Purchase failed"
+ suggestedCurrency: "USD",
+ expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`,
+ buyStatus: http.StatusPaymentRequired,
+ buyErrorCode: "request-failed",
+ buyErrorMessage: "Purchase failed",
+ expectedError: "payment declined",
+ },
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreBuy500(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ }))
+
+ detailsURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+ customersMeURI, err := url.Parse(mockPurchasesServer.URL + customersMePath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ CustomersMeURI: customersMeURI,
+ DetailsURI: detailsURI,
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ buyOptions := &BuyOptions{
+ SnapID: helloWorldSnapID,
+ Currency: "USD",
+ Price: 1,
+ }
+ _, err = repo.Buy(buyOptions, t.user)
+ c.Assert(err, NotNil)
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreBuy(c *C) {
+ for _, test := range buyTests {
+ searchServerCalled := false
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/hello-world")
+ w.Header().Set("Content-Type", "application/hal+json")
+ w.Header().Set("X-Suggested-Currency", test.suggestedCurrency)
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, MockDetailsJSON)
+ searchServerCalled = true
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ purchaseServerGetCalled := false
+ purchaseServerPostCalled := false
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case "GET":
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ switch r.URL.Path {
+ case ordersPath:
+ io.WriteString(w, `{"orders": []}`)
+ case customersMePath:
+ io.WriteString(w, customersMeValid)
+ default:
+ c.Fail()
+ }
+ purchaseServerGetCalled = true
+ case "POST":
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.Header.Get("Content-Type"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, ordersPath)
+ jsonReq, err := ioutil.ReadAll(r.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(jsonReq), Equals, test.expectedInput)
+ if test.buyErrorCode == "" {
+ io.WriteString(w, test.buyResponse)
+ } else {
+ w.WriteHeader(test.buyStatus)
+ fmt.Fprintf(w, `
+{
+ "error_list": [
+ {
+ "code": "%s",
+ "message": "%s"
+ }
+ ]
+}`, test.buyErrorCode, test.buyErrorMessage)
+ }
+
+ purchaseServerPostCalled = true
+ default:
+ c.Error("Unexpected request method: ", r.Method)
+ }
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ detailsURI, err := url.Parse(mockServer.URL)
+ c.Assert(err, IsNil)
+ ordersURI, err := url.Parse(mockPurchasesServer.URL + ordersPath)
+ c.Assert(err, IsNil)
+ customersMeURI, err := url.Parse(mockPurchasesServer.URL + customersMePath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ CustomersMeURI: customersMeURI,
+ DetailsURI: detailsURI,
+ OrdersURI: ordersURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ // Find the snap first
+ spec := SnapSpec{
+ Name: "hello-world",
+ Channel: "edge",
+ Revision: snap.R(0),
+ }
+ snap, err := repo.SnapInfo(spec, t.user)
+ c.Assert(snap, NotNil)
+ c.Assert(err, IsNil)
+
+ buyOptions := &BuyOptions{
+ SnapID: snap.SnapID,
+ Currency: repo.SuggestedCurrency(),
+ Price: snap.Prices[repo.SuggestedCurrency()],
+ }
+ if test.snapID != "" {
+ buyOptions.SnapID = test.snapID
+ }
+ if test.currency != "" {
+ buyOptions.Currency = test.currency
+ }
+ if test.price > 0 {
+ buyOptions.Price = test.price
+ }
+ result, err := repo.Buy(buyOptions, t.user)
+
+ c.Check(result, DeepEquals, test.expectedResult)
+ if test.expectedError == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, test.expectedError)
+ }
+
+ c.Check(searchServerCalled, Equals, true)
+ c.Check(purchaseServerGetCalled, Equals, true)
+ c.Check(purchaseServerPostCalled, Equals, true)
+ }
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailArgumentChecking(c *C) {
+ repo := New(&Config{}, nil)
+ c.Assert(repo, NotNil)
+
+ // no snap ID
+ result, err := repo.Buy(&BuyOptions{
+ Price: 1.0,
+ Currency: "USD",
+ }, t.user)
+ c.Assert(result, IsNil)
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot buy snap: snap ID missing")
+
+ // no price
+ result, err = repo.Buy(&BuyOptions{
+ SnapID: "snap ID",
+ Currency: "USD",
+ }, t.user)
+ c.Assert(result, IsNil)
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot buy snap: invalid expected price")
+
+ // no currency
+ result, err = repo.Buy(&BuyOptions{
+ SnapID: "snap ID",
+ Price: 1.0,
+ }, t.user)
+ c.Assert(result, IsNil)
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot buy snap: currency missing")
+
+ // no user
+ result, err = repo.Buy(&BuyOptions{
+ SnapID: "snap ID",
+ Price: 1.0,
+ Currency: "USD",
+ }, nil)
+ c.Assert(result, IsNil)
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "you need to log in first")
+}
+
+var readyToBuyTests = []struct {
+ Input func(w http.ResponseWriter)
+ Test func(c *C, err error)
+ NumOfCalls int
+}{
+ {
+ // A user account the is ready for buying
+ Input: func(w http.ResponseWriter) {
+ io.WriteString(w, `
+{
+ "latest_tos_date": "2016-09-14T00:00:00+00:00",
+ "accepted_tos_date": "2016-09-14T15:56:49+00:00",
+ "latest_tos_accepted": true,
+ "has_payment_method": true
+}
+`)
+ },
+ Test: func(c *C, err error) {
+ c.Check(err, IsNil)
+ },
+ NumOfCalls: 1,
+ },
+ {
+ // A user account that hasn't accepted the TOS
+ Input: func(w http.ResponseWriter) {
+ io.WriteString(w, `
+{
+ "latest_tos_date": "2016-10-14T00:00:00+00:00",
+ "accepted_tos_date": "2016-09-14T15:56:49+00:00",
+ "latest_tos_accepted": false,
+ "has_payment_method": true
+}
+`)
+ },
+ Test: func(c *C, err error) {
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "terms of service not accepted")
+ },
+ NumOfCalls: 1,
+ },
+ {
+ // A user account that has no payment method
+ Input: func(w http.ResponseWriter) {
+ io.WriteString(w, `
+{
+ "latest_tos_date": "2016-10-14T00:00:00+00:00",
+ "accepted_tos_date": "2016-09-14T15:56:49+00:00",
+ "latest_tos_accepted": true,
+ "has_payment_method": false
+}
+`)
+ },
+ Test: func(c *C, err error) {
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "no payment methods")
+ },
+ NumOfCalls: 1,
+ },
+ {
+ // A user account that has no payment method and has not accepted the TOS
+ Input: func(w http.ResponseWriter) {
+ io.WriteString(w, `
+{
+ "latest_tos_date": "2016-10-14T00:00:00+00:00",
+ "accepted_tos_date": "2016-09-14T15:56:49+00:00",
+ "latest_tos_accepted": false,
+ "has_payment_method": false
+}
+`)
+ },
+ Test: func(c *C, err error) {
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "no payment methods")
+ },
+ NumOfCalls: 1,
+ },
+ {
+ // No user account exists
+ Input: func(w http.ResponseWriter) {
+ w.WriteHeader(http.StatusNotFound)
+ io.WriteString(w, "{}")
+ },
+ Test: func(c *C, err error) {
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, "cannot get customer details: server says no account exists")
+ },
+ NumOfCalls: 1,
+ },
+ {
+ // An unknown set of errors occurs
+ Input: func(w http.ResponseWriter) {
+ w.WriteHeader(http.StatusInternalServerError)
+ io.WriteString(w, `
+{
+ "error_list": [
+ {
+ "code": "code 1",
+ "message": "message 1"
+ },
+ {
+ "code": "code 2",
+ "message": "message 2"
+ }
+ ]
+}`)
+ },
+ Test: func(c *C, err error) {
+ c.Assert(err, NotNil)
+ c.Check(err.Error(), Equals, `store reported an error: message 1`)
+ },
+ NumOfCalls: 5,
+ },
+}
+
+func (t *remoteRepoTestSuite) TestUbuntuStoreReadyToBuy(c *C) {
+ for _, test := range readyToBuyTests {
+ purchaseServerGetCalled := 0
+ mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case "GET":
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
+ c.Check(r.URL.Path, Equals, customersMePath)
+ test.Input(w)
+ purchaseServerGetCalled++
+ default:
+ c.Error("Unexpected request method: ", r.Method)
+ }
+ }))
+
+ c.Assert(mockPurchasesServer, NotNil)
+ defer mockPurchasesServer.Close()
+
+ customersMeURI, err := url.Parse(mockPurchasesServer.URL + customersMePath)
+ c.Assert(err, IsNil)
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ cfg := Config{
+ CustomersMeURI: customersMeURI,
+ }
+ repo := New(&cfg, authContext)
+ c.Assert(repo, NotNil)
+
+ err = repo.ReadyToBuy(t.user)
+ test.Test(c, err)
+ c.Check(purchaseServerGetCalled, Equals, test.NumOfCalls)
+ }
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestSetRangeHeaderOnRedirect(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ http.Redirect(w, r, r.URL.Path+"-else", 302)
+ n++
+ case 1:
+ c.Check(r.URL.Path, Equals, "/somewhere-else")
+ rg := r.Header.Get("Range")
+ c.Check(rg, Equals, "bytes=5-")
+ default:
+ panic("got more than 2 requests in this test")
+ }
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ url, err := url.Parse(mockServer.URL + "/somewhere")
+ c.Assert(err, IsNil)
+ reqOptions := &requestOptions{
+ Method: "GET",
+ URL: url,
+ ExtraHeaders: map[string]string{
+ "Range": "bytes=5-",
+ },
+ }
+
+ sto := New(&Config{}, nil)
+ _, err = sto.doRequest(context.TODO(), sto.client, reqOptions, t.user)
+ c.Assert(err, IsNil)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "gopkg.in/retry.v1"
+)
+
+var (
+ httpClient = newHTTPClient(&httpClientOpts{
+ Timeout: 10 * time.Second,
+ MayLogBody: true,
+ })
+)
+
+type keysReply struct {
+ Username string `json:"username"`
+ SSHKeys []string `json:"ssh_keys"`
+ OpenIDIdentifier string `json:"openid_identifier"`
+}
+
+type User struct {
+ Username string
+ SSHKeys []string
+ OpenIDIdentifier string
+}
+
+func UserInfo(email string) (userinfo *User, err error) {
+ ssourl := fmt.Sprintf("%s/keys/%s", authURL(), url.QueryEscape(email))
+ for attempt := retry.Start(defaultRetryStrategy, nil); attempt.Next(); {
+ var resp *http.Response
+ resp, err = httpClient.Get(ssourl)
+ if err != nil {
+ if shouldRetryError(attempt, err) {
+ continue
+ }
+ break
+ }
+
+ if shouldRetryHttpResponse(attempt, resp) {
+ resp.Body.Close()
+ continue
+ }
+
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case 200: // good
+ case 404:
+ return nil, fmt.Errorf("cannot find user %q", email)
+ default:
+ return nil, respToError(resp, fmt.Sprintf("look up user %q", email))
+ }
+
+ var v keysReply
+ dec := json.NewDecoder(resp.Body)
+ if err = dec.Decode(&v); err != nil {
+ if shouldRetryError(attempt, err) {
+ continue
+ }
+ return nil, fmt.Errorf("cannot unmarshal: %s", err)
+ }
+
+ return &User{
+ Username: v.Username,
+ SSHKeys: v.SSHKeys,
+ OpenIDIdentifier: v.OpenIDIdentifier,
+ }, nil
+ }
+
+ return nil, err
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "time"
+
+ "gopkg.in/check.v1"
+ "gopkg.in/retry.v1"
+
+ "github.com/snapcore/snapd/testutil"
+)
+
+type userInfoSuite struct {
+ testutil.BaseTest
+}
+
+var _ = check.Suite(&userInfoSuite{})
+
+// obtained via:
+// `curl https://login.staging.ubuntu.com/api/v2/keys/mvo@ubuntu.com`
+// `curl https://login.staging.ubuntu.com/api/v2/keys/xDPXBdB`
+var mockServerJSON = `{
+ "username": "mvo",
+ "ssh_keys": [
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAqwsTkky+laeukWyGFmtiAQUFgjD+wKYuRtOj11gjTe3qUNDgMR54W8IUELZ6NwNWs2wium+jQZLY4vlsDq4PkYK8J2qgjRZURCKp4JbjbVNSg2WO7vDtl+0FIC1GaCdglRVWffrwKN1RLlwqBCVXi01nnTk3+hEpWddjqoTXMwM= egon@top",
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKBFmfD1KNULZv35907+ArIfxdGGzF1XCQj287AgK7k5GWcEdnUQfkSUHRZ4cNOqshY6W3CyDzVAmaDmeB9A7qpmsVlQp2D8y253+F2NMm1bcDdT3weG5vxkdF5qdx99gRMwDYJ4WZgIryrCAOqDLKmoSEuyuh1Zil9pDGPh/grf+EgXzDFnntgE8XJVKIldsbUplCmycSNtk47PtJATJ8q5v2dIazlxwmxKfarXS7x805u4ElrZ2h3JMCOOfL1k3sJbYc4JbZ6zB8DAhSsZ79KrStn3DE+gULmPJjM0HEbtouegZpE5wcHldoo4Oi78uNrwtv1lWp4AnK/Xwm3bl/ egon@bod\r\n"
+ ],
+ "openid_identifier": "xDPXBdB"
+}`
+
+func (t *userInfoSuite) SetUpTest(c *check.C) {
+ MockDefaultRetryStrategy(&t.BaseTest, retry.LimitCount(6, retry.LimitTime(1*time.Second,
+ retry.Exponential{
+ Initial: 1 * time.Millisecond,
+ Factor: 1.1,
+ },
+ )))
+}
+
+func (s *userInfoSuite) redirectToTestSSO(handler func(http.ResponseWriter, *http.Request)) {
+ server := httptest.NewServer(http.HandlerFunc(handler))
+ s.BaseTest.AddCleanup(func() { server.Close() })
+ os.Setenv("SNAPPY_FORCE_SSO_URL", server.URL+"/api/v2")
+ s.BaseTest.AddCleanup(func() { os.Unsetenv("SNAPPY_FORCE_SSO_URL") })
+}
+
+func (s *userInfoSuite) TestCreateUser(c *check.C) {
+ n := 0
+ s.redirectToTestSSO(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0, 1:
+ w.WriteHeader(http.StatusInternalServerError) // force retry of the request
+ case 2:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/api/v2/keys/popper@lse.ac.uk")
+ fmt.Fprintln(w, mockServerJSON)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+
+ info, err := UserInfo("popper@lse.ac.uk")
+ c.Assert(err, check.IsNil)
+ c.Assert(n, check.Equals, 3) // number of requests after retries
+ c.Check(info.Username, check.Equals, "mvo")
+ c.Check(info.OpenIDIdentifier, check.Equals, "xDPXBdB")
+ c.Check(info.SSHKeys, check.DeepEquals, []string{"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAqwsTkky+laeukWyGFmtiAQUFgjD+wKYuRtOj11gjTe3qUNDgMR54W8IUELZ6NwNWs2wium+jQZLY4vlsDq4PkYK8J2qgjRZURCKp4JbjbVNSg2WO7vDtl+0FIC1GaCdglRVWffrwKN1RLlwqBCVXi01nnTk3+hEpWddjqoTXMwM= egon@top",
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKBFmfD1KNULZv35907+ArIfxdGGzF1XCQj287AgK7k5GWcEdnUQfkSUHRZ4cNOqshY6W3CyDzVAmaDmeB9A7qpmsVlQp2D8y253+F2NMm1bcDdT3weG5vxkdF5qdx99gRMwDYJ4WZgIryrCAOqDLKmoSEuyuh1Zil9pDGPh/grf+EgXzDFnntgE8XJVKIldsbUplCmycSNtk47PtJATJ8q5v2dIazlxwmxKfarXS7x805u4ElrZ2h3JMCOOfL1k3sJbYc4JbZ6zB8DAhSsZ79KrStn3DE+gULmPJjM0HEbtouegZpE5wcHldoo4Oi78uNrwtv1lWp4AnK/Xwm3bl/ egon@bod\r\n"})
+}
+
+func (s *userInfoSuite) TestCreateUser500RetriesExhausted(c *check.C) {
+ n := 0
+ s.redirectToTestSSO(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ n++
+ })
+
+ _, err := UserInfo("popper@lse.ac.uk")
+ c.Assert(err, check.NotNil)
+ c.Assert(err, check.ErrorMatches, `cannot look up user.*?got unexpected HTTP status code 500.*`)
+ c.Assert(n, check.Equals, 6)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build withtestkeys
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+func init() {
+ // mark as testing if we carry testing keys
+ isTesting = true
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package strutil
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func init() {
+ // golang does not init Seed() itself
+ rand.Seed(time.Now().UTC().UnixNano())
+}
+
+const letters = "BCDFGHJKLMNPQRSTVWXYbcdfghjklmnpqrstvwxy0123456789"
+
+// MakeRandomString returns a random string of length length
+//
+// The vowels are omitted to avoid that words are created by pure
+// chance. Numbers are included.
+func MakeRandomString(length int) string {
+ out := ""
+ for i := 0; i < length; i++ {
+ out += string(letters[rand.Intn(len(letters))])
+ }
+
+ return out
+}
+
+// Convert the given size in btes to a readable string
+func SizeToStr(size int64) string {
+ suffixes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
+ for _, suf := range suffixes {
+ if size < 1000 {
+ return fmt.Sprintf("%d%s", size, suf)
+ }
+ size /= 1000
+ }
+ panic("SizeToStr got a size bigger than math.MaxInt64")
+}
+
+// Quoted formats a slice of strings to a quoted list of
+// comma-separated strings, e.g. `"snap1", "snap2"`
+func Quoted(names []string) string {
+ quoted := make([]string, len(names))
+ for i, name := range names {
+ quoted[i] = strconv.Quote(name)
+ }
+
+ return strings.Join(quoted, ", ")
+}
+
+// WordWrap takes a input string and word wraps after `n` chars
+// into a new slice.
+//
+// Caveats:
+// - If a single word that is biger than max will not get wrapped
+// - Extra whitespace will be removed
+func WordWrap(s string, max int) []string {
+ n := 0
+
+ var out []string
+ line := bytes.NewBuffer(nil)
+ // FIXME: we want to be smarter here. to quote Gustavo: "The
+ // logic here is corrupting the spacing of the original line,
+ // which means indentation and tabling will be gone. A better
+ // approach would be finding the break point and then using
+ // the original content instead of rewriting it."
+ for _, word := range strings.Fields(s) {
+ if n+len(word) > max && n > 0 {
+ out = append(out, line.String())
+ line.Truncate(0)
+ n = 0
+ } else if n > 0 {
+ fmt.Fprintf(line, " ")
+ n += 1
+ }
+ fmt.Fprintf(line, word)
+ n += len(word)
+ }
+ if line.Len() > 0 {
+ out = append(out, line.String())
+ }
+
+ return out
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package strutil_test
+
+import (
+ "math"
+ "math/rand"
+ "testing"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/strutil"
+)
+
+func Test(t *testing.T) { check.TestingT(t) }
+
+type strutilSuite struct{}
+
+var _ = check.Suite(&strutilSuite{})
+
+func (ts *strutilSuite) TestMakeRandomString(c *check.C) {
+ // for our tests
+ rand.Seed(1)
+
+ s1 := strutil.MakeRandomString(10)
+ c.Assert(s1, check.Equals, "pw7MpXh0JB")
+
+ s2 := strutil.MakeRandomString(5)
+ c.Assert(s2, check.Equals, "4PQyl")
+}
+
+func (*strutilSuite) TestQuoted(c *check.C) {
+ for _, t := range []struct {
+ in []string
+ out string
+ }{
+ {nil, ""},
+ {[]string{}, ""},
+ {[]string{"one"}, `"one"`},
+ {[]string{"one", "two"}, `"one", "two"`},
+ {[]string{"one", `tw"`}, `"one", "tw\""`},
+ } {
+ c.Check(strutil.Quoted(t.in), check.Equals, t.out, check.Commentf("expected %#v -> %s", t.in, t.out))
+ }
+}
+
+func (ts *strutilSuite) TestSizeToStr(c *check.C) {
+ for _, t := range []struct {
+ size int64
+ str string
+ }{
+ {0, "0B"},
+ {1, "1B"},
+ {400, "400B"},
+ {1000, "1kB"},
+ {1000 + 1, "1kB"},
+ {900 * 1000, "900kB"},
+ {1000 * 1000, "1MB"},
+ {20 * 1000 * 1000, "20MB"},
+ {1000 * 1000 * 1000, "1GB"},
+ {31 * 1000 * 1000 * 1000, "31GB"},
+ {math.MaxInt64, "9EB"},
+ } {
+ c.Check(strutil.SizeToStr(t.size), check.Equals, t.str)
+ }
+}
+
+func (ts *strutilSuite) TestWordWrap(c *check.C) {
+ for _, t := range []struct {
+ in string
+ out []string
+ n int
+ }{
+ // pathological
+ {"12345", []string{"12345"}, 3},
+ {"123 456789", []string{"123", "456789"}, 3},
+ // valid
+ {"abc def ghi", []string{"abc", "def", "ghi"}, 3},
+ {"a b c d e f", []string{"a b", "c d", "e f"}, 3},
+ {"ab cd ef", []string{"ab cd", "ef"}, 5},
+ // intentional (but slightly strange)
+ {"ab cd", []string{"ab", "cd"}, 2},
+ } {
+ c.Check(strutil.WordWrap(t.in, t.n), check.DeepEquals, t.out)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package strutil
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+const (
+ reDigit = "[0-9]"
+ reAlpha = "[a-zA-Z]"
+ reDigitOrNonDigit = "[0-9]+|[^0-9]+"
+
+ reHasEpoch = "^[0-9]+:"
+)
+
+var (
+ matchDigit = regexp.MustCompile(reDigit).Match
+ matchAlpha = regexp.MustCompile(reAlpha).Match
+ findFrags = regexp.MustCompile(reDigitOrNonDigit).FindAllString
+ matchEpoch = regexp.MustCompile(reHasEpoch).MatchString
+)
+
+// golang: seriously? that's sad!
+func max(a, b int) int {
+ if a < b {
+ return b
+ }
+ return a
+}
+
+// version number compare, inspired by the libapt/python-debian code
+func cmpInt(intA, intB int) int {
+ if intA < intB {
+ return -1
+ } else if intA > intB {
+ return 1
+ }
+ return 0
+}
+
+func chOrder(ch uint8) int {
+ if ch == '~' {
+ return -1
+ }
+ if matchAlpha([]byte{ch}) {
+ return int(ch)
+ }
+
+ // can only happen if cmpString sets '0' because there is no fragment
+ if matchDigit([]byte{ch}) {
+ return 0
+ }
+
+ return int(ch) + 256
+}
+
+func cmpString(as, bs string) int {
+ for i := 0; i < max(len(as), len(bs)); i++ {
+ a := uint8('0')
+ b := uint8('0')
+ if i < len(as) {
+ a = as[i]
+ }
+ if i < len(bs) {
+ b = bs[i]
+ }
+ if chOrder(a) < chOrder(b) {
+ return -1
+ }
+ if chOrder(a) > chOrder(b) {
+ return +1
+ }
+ }
+ return 0
+}
+
+func cmpFragment(a, b string) int {
+ intA, errA := strconv.Atoi(a)
+ intB, errB := strconv.Atoi(b)
+ if errA == nil && errB == nil {
+ return cmpInt(intA, intB)
+ }
+ res := cmpString(a, b)
+ //fmt.Println(a, b, res)
+ return res
+}
+
+func getFragments(a string) []string {
+ return findFrags(a, -1)
+}
+
+// VersionIsValid returns true if the given string is a valid
+// version number according to the debian policy
+func VersionIsValid(a string) bool {
+ if matchEpoch(a) {
+ return false
+ }
+ if strings.Count(a, "-") > 1 {
+ return false
+ }
+ if strings.TrimSpace(a) == "" {
+ return false
+ }
+ return true
+}
+
+func compareSubversion(va, vb string) int {
+ fragsA := getFragments(va)
+ fragsB := getFragments(vb)
+
+ for i := 0; i < max(len(fragsA), len(fragsB)); i++ {
+ a := "0"
+ b := "0"
+ if i < len(fragsA) {
+ a = fragsA[i]
+ }
+ if i < len(fragsB) {
+ b = fragsB[i]
+ }
+ res := cmpFragment(a, b)
+ //fmt.Println(a, b, res)
+ if res != 0 {
+ return res
+ }
+ }
+ return 0
+}
+
+// VersionCompare compare two version strings that follow the debian
+// version policy and
+// Returns:
+// -1 if a is smaller than b
+// 0 if a equals b
+// +1 if a is bigger than b
+func VersionCompare(va, vb string) (res int, err error) {
+ // FIXME: return err here instead
+ if !VersionIsValid(va) {
+ return 0, fmt.Errorf("invalid version %q", va)
+ }
+ if !VersionIsValid(vb) {
+ return 0, fmt.Errorf("invalid version %q", vb)
+ }
+
+ if !strings.Contains(va, "-") {
+ va += "-0"
+ }
+ if !strings.Contains(vb, "-") {
+ vb += "-0"
+ }
+
+ // the main version number (before the "-")
+ mainA := strings.Split(va, "-")[0]
+ mainB := strings.Split(vb, "-")[0]
+ res = compareSubversion(mainA, mainB)
+ if res != 0 {
+ return res, nil
+ }
+
+ // the subversion revision behind the "-"
+ revA := strings.Split(va, "-")[1]
+ revB := strings.Split(vb, "-")[1]
+ return compareSubversion(revA, revB), nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package strutil_test
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/strutil"
+)
+
+type VersionTestSuite struct{}
+
+var _ = Suite(&VersionTestSuite{})
+
+func (s *VersionTestSuite) TestVersionCompare(c *C) {
+ for _, t := range []struct {
+ A, B string
+ res int
+ err error
+ }{
+ {"1.0", "2.0", -1, nil},
+ {"1.3", "1.2.2.2", 1, nil},
+ {"1.3", "1.3.1", -1, nil},
+ {"1.0", "1.0~", 1, nil},
+ {"7.2p2", "7.2", 1, nil},
+ {"0.4a6", "0.4", 1, nil},
+ {"0pre", "0pre", 0, nil},
+ {"0pree", "0pre", 1, nil},
+ {"1.18.36:5.4", "1.18.36:5.5", -1, nil},
+ {"1.18.36:5.4", "1.18.37:1.1", -1, nil},
+ {"2.0.7pre1", "2.0.7r", -1, nil},
+ {"0.10.0", "0.8.7", 1, nil},
+ // subrev
+ {"1.0-1", "1.0-2", -1, nil},
+ {"1.0-1.1", "1.0-1", 1, nil},
+ {"1.0-1.1", "1.0-1.1", 0, nil},
+ // do we like strange versions? Yes we like strange versions…
+ {"0", "0", 0, nil},
+ {"0", "00", 0, nil},
+ // broken
+ {"0--0", "0", 0, fmt.Errorf(`invalid version "0--0"`)},
+ } {
+ res, err := strutil.VersionCompare(t.A, t.B)
+ if t.err != nil {
+ c.Check(err, DeepEquals, t.err)
+ } else {
+ c.Check(res, Equals, t.res, Commentf("%s %s: %v but got %v", t.A, t.B, res, t.res))
+ }
+ }
+}
+
+func (s *VersionTestSuite) TestVersionInvalid(c *C) {
+ for _, t := range []struct {
+ ver string
+ valid bool
+ }{
+ {"1:2", false},
+ {"1--1", false},
+ {"1.0", true},
+ } {
+ res := strutil.VersionIsValid(t.ver)
+ c.Check(res, Equals, t.valid, Commentf("%q: %v but expected %v", t.ver, res, t.valid))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+const allowed = `:_.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`
+
+// EscapeUnitNamePath works like systemd-escape --path
+// FIXME: we could use github.com/coreos/go-systemd/unit/escape.go
+// and EscapePath from it.
+//
+// But thats not in the archive and it won't work with go1.3
+func EscapeUnitNamePath(in string) string {
+ buf := bytes.NewBuffer(nil)
+
+ // clean and trim leading/trailing "/"
+ in = filepath.Clean(in)
+ in = strings.Trim(in, "/")
+
+ // empty strings is "/"
+ if len(in) == 0 {
+ in = "/"
+ }
+ // leading "." is special
+ if in[0] == '.' {
+ fmt.Fprintf(buf, `\x%x`, in[0])
+ in = in[1:]
+ }
+
+ // replace all special chars
+ for i := 0; i < len(in); i++ {
+ c := in[i]
+ if c == '/' {
+ buf.WriteByte('-')
+ } else if strings.IndexByte(allowed, c) >= 0 {
+ buf.WriteByte(c)
+ } else {
+ fmt.Fprintf(buf, `\x%x`, in[i])
+ }
+ }
+
+ return buf.String()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/snapd/systemd"
+)
+
+func (s *SystemdTestSuite) TestEscape(c *C) {
+ c.Check(EscapeUnitNamePath("Hallöchen, Meister"), Equals, `Hall\xc3\xb6chen\x2c\x20Meister`)
+
+ c.Check(EscapeUnitNamePath("/tmp//waldi/foobar/"), Equals, `tmp-waldi-foobar`)
+ c.Check(EscapeUnitNamePath("/.foo/.bar"), Equals, `\x2efoo-.bar`)
+ c.Check(EscapeUnitNamePath("////"), Equals, `-`)
+ c.Check(EscapeUnitNamePath("."), Equals, `\x2e`)
+ c.Check(EscapeUnitNamePath("/foo/bar-baz"), Equals, `foo-bar\x2dbaz`)
+ c.Check(EscapeUnitNamePath("/foo/bar--baz"), Equals, `foo-bar\x2d\x2dbaz`)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd
+
+import (
+ "time"
+)
+
+var (
+ SystemdRun = run // NOTE: plain Run clashes with check.v1
+ Jctl = jctl
+)
+
+func MockStopDelays(checkDelay, notifyDelay time.Duration) func() {
+ oldCheckDelay := stopCheckDelay
+ oldNotifyDelay := stopNotifyDelay
+ stopCheckDelay = checkDelay
+ stopNotifyDelay = notifyDelay
+ return func() {
+ stopCheckDelay = oldCheckDelay
+ stopNotifyDelay = oldNotifyDelay
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+var (
+ // the output of "show" must match this for Stop to be done:
+ isStopDone = regexp.MustCompile(`(?m)\AActiveState=(?:failed|inactive)$`).Match
+
+ // how much time should Stop wait between calls to show
+ stopCheckDelay = 250 * time.Millisecond
+
+ // how much time should Stop wait between notifying the user of the waiting
+ stopNotifyDelay = 20 * time.Second
+)
+
+// run calls systemctl with the given args, returning its standard output (and wrapped error)
+func run(args ...string) ([]byte, error) {
+ bs, err := exec.Command("systemctl", args...).CombinedOutput()
+ if err != nil {
+ exitCode, _ := osutil.ExitCode(err)
+ return nil, &Error{cmd: args, exitCode: exitCode, msg: bs}
+ }
+
+ return bs, nil
+}
+
+// SystemctlCmd is called from the commands to actually call out to
+// systemctl. It's exported so it can be overridden by testing.
+var SystemctlCmd = run
+
+// jctl calls journalctl to get the JSON logs of the given services, wrapping the error if any.
+func jctl(svcs []string) ([]byte, error) {
+ cmd := []string{"journalctl", "-o", "json"}
+
+ for i := range svcs {
+ cmd = append(cmd, "-u", svcs[i])
+ }
+
+ bs, err := exec.Command(cmd[0], cmd[1:]...).Output() // journalctl can be messy with its stderr
+ if err != nil {
+ exitCode, _ := osutil.ExitCode(err)
+ return nil, &Error{cmd: cmd, exitCode: exitCode, msg: bs}
+ }
+
+ return bs, nil
+}
+
+// JournalctlCmd is called from Logs to run journalctl; exported for testing.
+var JournalctlCmd = jctl
+
+// Systemd exposes a minimal interface to manage systemd via the systemctl command.
+type Systemd interface {
+ DaemonReload() error
+ Enable(service string) error
+ Disable(service string) error
+ Start(service string) error
+ Stop(service string, timeout time.Duration) error
+ Kill(service, signal string) error
+ Restart(service string, timeout time.Duration) error
+ Status(service string) (string, error)
+ ServiceStatus(service string) (*ServiceStatus, error)
+ Logs(services []string) ([]Log, error)
+ WriteMountUnitFile(name, what, where, fstype string) (string, error)
+}
+
+// A Log is a single entry in the systemd journal
+type Log map[string]interface{}
+
+// RestartCondition encapsulates the different systemd 'restart' options
+type RestartCondition string
+
+// These are the supported restart conditions
+const (
+ RestartNever RestartCondition = "never"
+ RestartOnSuccess RestartCondition = "on-success"
+ RestartOnFailure RestartCondition = "on-failure"
+ RestartOnAbnormal RestartCondition = "on-abnormal"
+ RestartOnAbort RestartCondition = "on-abort"
+ RestartAlways RestartCondition = "always"
+)
+
+var RestartMap = map[string]RestartCondition{
+ "never": RestartNever,
+ "on-success": RestartOnSuccess,
+ "on-failure": RestartOnFailure,
+ "on-abnormal": RestartOnAbnormal,
+ "on-abort": RestartOnAbort,
+ "always": RestartAlways,
+}
+
+// ErrUnknownRestartCondition is returned when trying to unmarshal an unknown restart condition
+var ErrUnknownRestartCondition = errors.New("invalid restart condition")
+
+func (rc RestartCondition) String() string {
+ return string(rc)
+}
+
+// UnmarshalYAML so RestartCondition implements yaml's Unmarshaler interface
+func (rc *RestartCondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var v string
+
+ if err := unmarshal(&v); err != nil {
+ return err
+ }
+
+ nrc, ok := RestartMap[v]
+ if !ok {
+ return ErrUnknownRestartCondition
+ }
+
+ *rc = nrc
+
+ return nil
+}
+
+const (
+ // the default target for systemd units that we generate
+ ServicesTarget = "multi-user.target"
+
+ // the target prerequisite for systemd units we generate
+ PrerequisiteTarget = "network-online.target"
+
+ // the default target for systemd units that we generate
+ SocketsTarget = "sockets.target"
+)
+
+type reporter interface {
+ Notify(string)
+}
+
+// New returns a Systemd that uses the given rootDir
+func New(rootDir string, rep reporter) Systemd {
+ return &systemd{rootDir: rootDir, reporter: rep}
+}
+
+type systemd struct {
+ rootDir string
+ reporter reporter
+}
+
+// DaemonReload reloads systemd's configuration.
+func (*systemd) DaemonReload() error {
+ _, err := SystemctlCmd("daemon-reload")
+ return err
+}
+
+// Enable the given service
+func (s *systemd) Enable(serviceName string) error {
+ _, err := SystemctlCmd("--root", s.rootDir, "enable", serviceName)
+ return err
+}
+
+// Disable the given service
+func (s *systemd) Disable(serviceName string) error {
+ _, err := SystemctlCmd("--root", s.rootDir, "disable", serviceName)
+ return err
+}
+
+// Start the given service
+func (*systemd) Start(serviceName string) error {
+ _, err := SystemctlCmd("start", serviceName)
+ return err
+}
+
+// Logs for the given service
+func (*systemd) Logs(serviceNames []string) ([]Log, error) {
+ bs, err := JournalctlCmd(serviceNames)
+ if err != nil {
+ return nil, err
+ }
+
+ const noEntries = "-- No entries --\n"
+ if len(bs) == len(noEntries) && string(bs) == noEntries {
+ return nil, nil
+ }
+
+ var logs []Log
+ dec := json.NewDecoder(bytes.NewReader(bs))
+ for {
+ var log Log
+
+ err = dec.Decode(&log)
+ if err != nil {
+ break
+ }
+
+ logs = append(logs, log)
+ }
+
+ if err != io.EOF {
+ return nil, err
+ }
+
+ return logs, nil
+}
+
+var statusregex = regexp.MustCompile(`(?m)^(?:(.*?)=(.*))?$`)
+
+func (s *systemd) Status(serviceName string) (string, error) {
+ status, err := s.ServiceStatus(serviceName)
+ if err != nil {
+ return "", err
+ }
+
+ return fmt.Sprintf("%s; %s; %s (%s)", status.UnitFileState, status.LoadState, status.ActiveState, status.SubState), nil
+}
+
+// A ServiceStatus holds structured service status information.
+type ServiceStatus struct {
+ ServiceFileName string `json:"service-file-name"`
+ LoadState string `json:"load-state"`
+ ActiveState string `json:"active-state"`
+ SubState string `json:"sub-state"`
+ UnitFileState string `json:"unit-file-state"`
+}
+
+func (s *systemd) ServiceStatus(serviceName string) (*ServiceStatus, error) {
+ bs, err := SystemctlCmd("show", "--property=Id,LoadState,ActiveState,SubState,UnitFileState", serviceName)
+ if err != nil {
+ return nil, err
+ }
+
+ status := &ServiceStatus{ServiceFileName: serviceName}
+
+ for _, bs := range statusregex.FindAllSubmatch(bs, -1) {
+ if len(bs[0]) > 0 {
+ k := string(bs[1])
+ v := string(bs[2])
+ switch k {
+ case "LoadState":
+ status.LoadState = v
+ case "ActiveState":
+ status.ActiveState = v
+ case "SubState":
+ status.SubState = v
+ case "UnitFileState":
+ status.UnitFileState = v
+ }
+ }
+ }
+
+ return status, nil
+}
+
+// Stop the given service, and wait until it has stopped.
+func (s *systemd) Stop(serviceName string, timeout time.Duration) error {
+ if _, err := SystemctlCmd("stop", serviceName); err != nil {
+ return err
+ }
+
+ // and now wait for it to actually stop
+ giveup := time.NewTimer(timeout)
+ notify := time.NewTicker(stopNotifyDelay)
+ defer notify.Stop()
+ check := time.NewTicker(stopCheckDelay)
+ defer check.Stop()
+
+ firstCheck := true
+loop:
+ for {
+ select {
+ case <-giveup.C:
+ break loop
+ case <-check.C:
+ bs, err := SystemctlCmd("show", "--property=ActiveState", serviceName)
+ if err != nil {
+ return err
+ }
+ if isStopDone(bs) {
+ return nil
+ }
+ if !firstCheck {
+ continue loop
+ }
+ firstCheck = false
+ case <-notify.C:
+ }
+ // after notify delay or after a failed first check
+ s.reporter.Notify(fmt.Sprintf("Waiting for %s to stop.", serviceName))
+ }
+
+ return &Timeout{action: "stop", service: serviceName}
+}
+
+// Kill all processes of the unit with the given signal
+func (s *systemd) Kill(serviceName, signal string) error {
+ _, err := SystemctlCmd("kill", serviceName, "-s", signal)
+ return err
+}
+
+// Restart the service, waiting for it to stop before starting it again.
+func (s *systemd) Restart(serviceName string, timeout time.Duration) error {
+ if err := s.Stop(serviceName, timeout); err != nil {
+ return err
+ }
+ return s.Start(serviceName)
+}
+
+// Error is returned if the systemd action failed
+type Error struct {
+ cmd []string
+ msg []byte
+ exitCode int
+}
+
+func (e *Error) Error() string {
+ return fmt.Sprintf("%v failed with exit status %d: %s", e.cmd, e.exitCode, e.msg)
+}
+
+// Timeout is returned if the systemd action failed to reach the
+// expected state in a reasonable amount of time
+type Timeout struct {
+ action string
+ service string
+}
+
+func (e *Timeout) Error() string {
+ return fmt.Sprintf("%v failed to %v: timeout", e.service, e.action)
+}
+
+// IsTimeout checks whether the given error is a Timeout
+func IsTimeout(err error) bool {
+ _, isTimeout := err.(*Timeout)
+ return isTimeout
+}
+
+const myFmt = "2006-01-02T15:04:05.000000Z07:00"
+
+// Timestamp of the Log, formatted like RFC3339 to µs precision.
+//
+// If no timestamp, the string "-(no timestamp!)-" -- and something is
+// wrong with your system. Some other "impossible" error conditions
+// also result in "-(errror message)-" timestamps.
+func (l Log) Timestamp() string {
+ t := "-(no timestamp!)-"
+ if ius, ok := l["__REALTIME_TIMESTAMP"]; ok {
+ // according to systemd.journal-fields(7) it's microseconds as a decimal string
+ sus, ok := ius.(string)
+ if ok {
+ if us, err := strconv.ParseInt(sus, 10, 64); err == nil {
+ t = time.Unix(us/1000000, 1000*(us%1000000)).UTC().Format(myFmt)
+ } else {
+ t = fmt.Sprintf("-(timestamp not a decimal number: %#v)-", sus)
+ }
+ } else {
+ t = fmt.Sprintf("-(timestamp not a string: %#v)-", ius)
+ }
+ }
+
+ return t
+}
+
+// Message of the Log, if any; otherwise, "-".
+func (l Log) Message() string {
+ if msg, ok := l["MESSAGE"].(string); ok {
+ return msg
+ }
+
+ return "-"
+}
+
+// SID is the syslog identifier of the Log, if any; otherwise, "-".
+func (l Log) SID() string {
+ if sid, ok := l["SYSLOG_IDENTIFIER"].(string); ok {
+ return sid
+ }
+
+ return "-"
+}
+
+func (l Log) String() string {
+ return fmt.Sprintf("%s %s %s", l.Timestamp(), l.SID(), l.Message())
+}
+
+// useFuse detects if we should be using squashfuse instead
+func useFuse() bool {
+ if !osutil.FileExists("/dev/fuse") {
+ return false
+ }
+
+ _, err := exec.LookPath("squashfuse")
+ if err != nil {
+ return false
+ }
+
+ out, err := exec.Command("systemd-detect-virt", "--container").Output()
+ if err != nil {
+ return false
+ }
+
+ virt := strings.TrimSpace(string(out))
+ if virt != "none" {
+ return true
+ }
+
+ return false
+}
+
+// MountUnitPath returns the path of a {,auto}mount unit
+func MountUnitPath(baseDir string) string {
+ escapedPath := EscapeUnitNamePath(baseDir)
+ return filepath.Join(dirs.SnapServicesDir, escapedPath+".mount")
+}
+
+func (s *systemd) WriteMountUnitFile(name, what, where, fstype string) (string, error) {
+ extra := ""
+ if osutil.IsDirectory(what) {
+ extra = "Options=bind\n"
+ fstype = "none"
+ } else if fstype == "squashfs" && useFuse() {
+ extra = "Options=ro,allow_other\n"
+ fstype = "fuse.squashfuse"
+ }
+
+ c := fmt.Sprintf(`[Unit]
+Description=Mount unit for %s
+
+[Mount]
+What=%s
+Where=%s
+Type=%s
+%s
+[Install]
+WantedBy=multi-user.target
+`, name, what, where, fstype, extra)
+
+ mu := MountUnitPath(where)
+ return filepath.Base(mu), osutil.AtomicWriteFile(mu, []byte(c), 0644, 0)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package systemd_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+
+ . "github.com/snapcore/snapd/systemd"
+)
+
+type testreporter struct {
+ msgs []string
+}
+
+func (tr *testreporter) Notify(msg string) {
+ tr.msgs = append(tr.msgs, msg)
+}
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+// systemd's testsuite
+type SystemdTestSuite struct {
+ i int
+ argses [][]string
+ errors []error
+ outs [][]byte
+
+ j int
+ jsvcs [][]string
+ jouts [][]byte
+ jerrs []error
+
+ rep *testreporter
+}
+
+var _ = Suite(&SystemdTestSuite{})
+
+func (s *SystemdTestSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ err := os.MkdirAll(dirs.SnapServicesDir, 0755)
+ c.Assert(err, IsNil)
+
+ // force UTC timezone, for reproducible timestamps
+ os.Setenv("TZ", "")
+
+ SystemctlCmd = s.myRun
+ s.i = 0
+ s.argses = nil
+ s.errors = nil
+ s.outs = nil
+
+ JournalctlCmd = s.myJctl
+ s.j = 0
+ s.jsvcs = nil
+ s.jouts = nil
+ s.jerrs = nil
+
+ s.rep = new(testreporter)
+}
+
+func (s *SystemdTestSuite) TearDownTest(c *C) {
+ SystemctlCmd = SystemdRun
+ JournalctlCmd = Jctl
+}
+
+func (s *SystemdTestSuite) myRun(args ...string) (out []byte, err error) {
+ s.argses = append(s.argses, args)
+ if s.i < len(s.outs) {
+ out = s.outs[s.i]
+ }
+ if s.i < len(s.errors) {
+ err = s.errors[s.i]
+ }
+ s.i++
+ return out, err
+}
+
+func (s *SystemdTestSuite) myJctl(svcs []string) (out []byte, err error) {
+ s.jsvcs = append(s.jsvcs, svcs)
+
+ if s.j < len(s.jouts) {
+ out = s.jouts[s.j]
+ }
+ if s.j < len(s.jerrs) {
+ err = s.jerrs[s.j]
+ }
+ s.j++
+
+ return out, err
+}
+
+func (s *SystemdTestSuite) TestDaemonReload(c *C) {
+ err := New("", s.rep).DaemonReload()
+ c.Assert(err, IsNil)
+ c.Assert(s.argses, DeepEquals, [][]string{{"daemon-reload"}})
+}
+
+func (s *SystemdTestSuite) TestStart(c *C) {
+ err := New("", s.rep).Start("foo")
+ c.Assert(err, IsNil)
+ c.Check(s.argses, DeepEquals, [][]string{{"start", "foo"}})
+}
+
+func (s *SystemdTestSuite) TestStop(c *C) {
+ restore := MockStopDelays(time.Millisecond, 25*time.Second)
+ defer restore()
+ s.outs = [][]byte{
+ nil, // for the "stop" itself
+ []byte("ActiveState=whatever\n"),
+ []byte("ActiveState=active\n"),
+ []byte("ActiveState=inactive\n"),
+ }
+ s.errors = []error{nil, nil, nil, nil, &Timeout{}}
+ err := New("", s.rep).Stop("foo", 1*time.Second)
+ c.Assert(err, IsNil)
+ c.Assert(s.argses, HasLen, 4)
+ c.Check(s.argses[0], DeepEquals, []string{"stop", "foo"})
+ c.Check(s.argses[1], DeepEquals, []string{"show", "--property=ActiveState", "foo"})
+ c.Check(s.argses[1], DeepEquals, s.argses[2])
+ c.Check(s.argses[1], DeepEquals, s.argses[3])
+}
+
+func (s *SystemdTestSuite) TestStatus(c *C) {
+ s.outs = [][]byte{
+ []byte("Id=Thing\nLoadState=LoadState\nActiveState=ActiveState\nSubState=SubState\nUnitFileState=UnitFileState\n"),
+ }
+ s.errors = []error{nil}
+ out, err := New("", s.rep).Status("foo")
+ c.Assert(err, IsNil)
+ c.Check(out, Equals, "UnitFileState; LoadState; ActiveState (SubState)")
+}
+
+func (s *SystemdTestSuite) TestStatusObj(c *C) {
+ s.outs = [][]byte{
+ []byte("Id=Thing\nLoadState=LoadState\nActiveState=ActiveState\nSubState=SubState\nUnitFileState=UnitFileState\n"),
+ }
+ s.errors = []error{nil}
+ out, err := New("", s.rep).ServiceStatus("foo")
+ c.Assert(err, IsNil)
+ c.Check(out, DeepEquals, &ServiceStatus{
+ ServiceFileName: "foo",
+ LoadState: "LoadState",
+ ActiveState: "ActiveState",
+ SubState: "SubState",
+ UnitFileState: "UnitFileState",
+ })
+}
+
+func (s *SystemdTestSuite) TestStopTimeout(c *C) {
+ restore := MockStopDelays(time.Millisecond, 25*time.Second)
+ defer restore()
+ err := New("", s.rep).Stop("foo", 10*time.Millisecond)
+ c.Assert(err, FitsTypeOf, &Timeout{})
+ c.Assert(len(s.rep.msgs) > 0, Equals, true)
+ c.Check(s.rep.msgs[0], Equals, "Waiting for foo to stop.")
+}
+
+func (s *SystemdTestSuite) TestDisable(c *C) {
+ err := New("xyzzy", s.rep).Disable("foo")
+ c.Assert(err, IsNil)
+ c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "disable", "foo"}})
+}
+
+func (s *SystemdTestSuite) TestEnable(c *C) {
+ err := New("xyzzy", s.rep).Enable("foo")
+ c.Assert(err, IsNil)
+ c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "enable", "foo"}})
+}
+
+func (s *SystemdTestSuite) TestRestart(c *C) {
+ restore := MockStopDelays(time.Millisecond, 25*time.Second)
+ defer restore()
+ s.outs = [][]byte{
+ nil, // for the "stop" itself
+ []byte("ActiveState=inactive\n"),
+ nil, // for the "start"
+ }
+ s.errors = []error{nil, nil, nil, nil, &Timeout{}}
+ err := New("", s.rep).Restart("foo", 100*time.Millisecond)
+ c.Assert(err, IsNil)
+ c.Check(s.argses, HasLen, 3)
+ c.Check(s.argses[0], DeepEquals, []string{"stop", "foo"})
+ c.Check(s.argses[1], DeepEquals, []string{"show", "--property=ActiveState", "foo"})
+ c.Check(s.argses[2], DeepEquals, []string{"start", "foo"})
+}
+
+func (s *SystemdTestSuite) TestKill(c *C) {
+ c.Assert(New("", s.rep).Kill("foo", "HUP"), IsNil)
+ c.Check(s.argses, DeepEquals, [][]string{{"kill", "foo", "-s", "HUP"}})
+}
+
+func (s *SystemdTestSuite) TestIsTimeout(c *C) {
+ c.Check(IsTimeout(os.ErrInvalid), Equals, false)
+ c.Check(IsTimeout(&Timeout{}), Equals, true)
+}
+
+func (s *SystemdTestSuite) TestLogErrJctl(c *C) {
+ s.jerrs = []error{&Timeout{}}
+
+ logs, err := New("", s.rep).Logs([]string{"foo"})
+ c.Check(err, NotNil)
+ c.Check(logs, IsNil)
+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
+ c.Check(s.j, Equals, 1)
+}
+
+func (s *SystemdTestSuite) TestLogErrJSON(c *C) {
+ s.jouts = [][]byte{[]byte("this is not valid json.")}
+
+ logs, err := New("", s.rep).Logs([]string{"foo"})
+ c.Check(err, NotNil)
+ c.Check(logs, IsNil)
+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
+ c.Check(s.j, Equals, 1)
+}
+
+func (s *SystemdTestSuite) TestLogs(c *C) {
+ s.jouts = [][]byte{[]byte(`{"a": 1}
+{"a": 2}
+`)}
+
+ logs, err := New("", s.rep).Logs([]string{"foo"})
+ c.Check(err, IsNil)
+ c.Check(logs, DeepEquals, []Log{{"a": 1.}, {"a": 2.}})
+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
+ c.Check(s.j, Equals, 1)
+}
+
+func (s *SystemdTestSuite) TestLogString(c *C) {
+ c.Check(Log{}.String(), Equals, "-(no timestamp!)- - -")
+ c.Check(Log{
+ "__REALTIME_TIMESTAMP": 42,
+ }.String(), Equals, "-(timestamp not a string: 42)- - -")
+ c.Check(Log{
+ "__REALTIME_TIMESTAMP": "what",
+ }.String(), Equals, "-(timestamp not a decimal number: \"what\")- - -")
+ c.Check(Log{
+ "__REALTIME_TIMESTAMP": "0",
+ "MESSAGE": "hi",
+ }.String(), Equals, "1970-01-01T00:00:00.000000Z - hi")
+ c.Check(Log{
+ "__REALTIME_TIMESTAMP": "42",
+ "MESSAGE": "hi",
+ "SYSLOG_IDENTIFIER": "me",
+ }.String(), Equals, "1970-01-01T00:00:00.000042Z me hi")
+
+}
+
+func (s *SystemdTestSuite) TestMountUnitPath(c *C) {
+ c.Assert(MountUnitPath("/apps/hello/1.1"), Equals, filepath.Join(dirs.SnapServicesDir, "apps-hello-1.1.mount"))
+}
+
+func (s *SystemdTestSuite) TestWriteMountUnit(c *C) {
+ mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap")
+ err := os.MkdirAll(filepath.Dir(mockSnapPath), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(mockSnapPath, nil, 0644)
+ c.Assert(err, IsNil)
+
+ mountUnitName, err := New("", nil).WriteMountUnitFile("foo", mockSnapPath, "/apps/foo/1.0", "squashfs")
+ c.Assert(err, IsNil)
+ defer os.Remove(mountUnitName)
+
+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, mountUnitName))
+ c.Assert(err, IsNil)
+ c.Assert(string(mount), Equals, fmt.Sprintf(`[Unit]
+Description=Mount unit for foo
+
+[Mount]
+What=%s
+Where=/apps/foo/1.0
+Type=squashfs
+
+[Install]
+WantedBy=multi-user.target
+`, mockSnapPath))
+}
+
+func (s *SystemdTestSuite) TestWriteMountUnitForDirs(c *C) {
+ // a directory instead of a file produces a different output
+ snapDir := c.MkDir()
+ mountUnitName, err := New("", nil).WriteMountUnitFile("foodir", snapDir, "/apps/foo/1.0", "squashfs")
+ c.Assert(err, IsNil)
+ defer os.Remove(mountUnitName)
+
+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, mountUnitName))
+ c.Assert(err, IsNil)
+ c.Assert(string(mount), Equals, fmt.Sprintf(`[Unit]
+Description=Mount unit for foodir
+
+[Mount]
+What=%s
+Where=/apps/foo/1.0
+Type=none
+Options=bind
+
+[Install]
+WantedBy=multi-user.target
+`, snapDir))
+}
+
+func (s *SystemdTestSuite) TestRestartCondUnmarshal(c *C) {
+ for cond := range RestartMap {
+ bs := []byte(cond)
+ var rc RestartCondition
+
+ c.Check(yaml.Unmarshal(bs, &rc), IsNil)
+ c.Check(rc, Equals, RestartMap[cond], Commentf(cond))
+ }
+}
+
+func (s *SystemdTestSuite) TestRestartCondString(c *C) {
+ for name, cond := range RestartMap {
+ c.Check(cond.String(), Equals, name, Commentf(name))
+ }
+}
+
+func (s *SystemdTestSuite) TestFuseInContainer(c *C) {
+ if !osutil.FileExists("/dev/fuse") {
+ c.Skip("No /dev/fuse on the system")
+ }
+
+ systemdCmd := testutil.MockCommand(c, "systemd-detect-virt", `
+echo lxc
+exit 0
+ `)
+ defer systemdCmd.Restore()
+
+ fuseCmd := testutil.MockCommand(c, "squashfuse", `
+exit 0
+ `)
+ defer fuseCmd.Restore()
+
+ mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap")
+ err := os.MkdirAll(filepath.Dir(mockSnapPath), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(mockSnapPath, nil, 0644)
+ c.Assert(err, IsNil)
+
+ mountUnitName, err := New("", nil).WriteMountUnitFile("foo", mockSnapPath, "/apps/foo/1.0", "squashfs")
+ c.Assert(err, IsNil)
+ defer os.Remove(mountUnitName)
+
+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, mountUnitName))
+ c.Assert(err, IsNil)
+ c.Assert(string(mount), Equals, fmt.Sprintf(`[Unit]
+Description=Mount unit for foo
+
+[Mount]
+What=%s
+Where=/apps/foo/1.0
+Type=fuse.squashfuse
+Options=ro,allow_other
+
+[Install]
+WantedBy=multi-user.target
+`, mockSnapPath))
+}
+
+func (s *SystemdTestSuite) TestFuseOutsideContainer(c *C) {
+ systemdCmd := testutil.MockCommand(c, "systemd-detect-virt", `
+echo none
+exit 0
+ `)
+ defer systemdCmd.Restore()
+
+ fuseCmd := testutil.MockCommand(c, "squashfuse", `
+exit 0
+ `)
+ defer fuseCmd.Restore()
+
+ mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap")
+ err := os.MkdirAll(filepath.Dir(mockSnapPath), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(mockSnapPath, nil, 0644)
+ c.Assert(err, IsNil)
+
+ mountUnitName, err := New("", nil).WriteMountUnitFile("foo", mockSnapPath, "/apps/foo/1.0", "squashfs")
+ c.Assert(err, IsNil)
+ defer os.Remove(mountUnitName)
+
+ mount, err := ioutil.ReadFile(filepath.Join(dirs.SnapServicesDir, mountUnitName))
+ c.Assert(err, IsNil)
+ c.Assert(string(mount), Equals, fmt.Sprintf(`[Unit]
+Description=Mount unit for foo
+
+[Mount]
+What=%s
+Where=/apps/foo/1.0
+Type=squashfs
+
+[Install]
+WantedBy=multi-user.target
+`, mockSnapPath))
+}
--- /dev/null
+Steps for executing snapd's spread suite on a running ubuntu-core instance:
+
+* Execute the console-conf setup on the device
+
+* From the host, set up the `SPREAD_EXTERNAL_ADDRESS` environment variable with
+the ip and port of the running instance:
+```
+$ export SPREAD_EXTERNAL_ADDRESS=<instance_ip>:<instance_port>
+```
+* From the snapd project's root execute the script to setup ssh access to the
+instance:
+```
+$ ./tests/lib/external/prepare-ssh.sh <instance_ip> <instance_port>
+```
+The default values for ip and port are `localhost`, `8022`. This script assumes that
+the user created by console-conf has the same name as the user executing the
+script, if that's not the case you can pass the created username as a third argument
+to the script.
+
+* From the snapd project's root execute the suite selecting the type of system of
+the instance, currently `ubuntu-core-16-64`, `ubuntu-core-16-arm-32` and
+`ubuntu-core-16-arm-64` are supported:
+```
+$ spread -v -reuse external:ubuntu-core-16-64
+```
+* You can execute again the suite by just reissuing the spread command, no need
+to run the prepare script again.
--- /dev/null
+#!/bin/bash
+
+apt_install_local() {
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ # relying on dpkg as apt(-get) does not support installation from local files in trusty.
+ dpkg -i --force-depends --force-depends-version "$@"
+ apt-get -f install -y
+ else
+ apt install -y "$@"
+ fi
+}
--- /dev/null
+type: model
+authority-id: developer1
+series: 16
+brand-id: developer1
+model: my-model
+architecture: amd64
+gadget: pc
+kernel: pc-kernel
+timestamp: 2016-08-19T15:58:42+02:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCV79lzwAAIGwQAJ6G8mPUhhUILLScaR/IGAdJ2R4HlbByNuY1OkTBvQGB4UIb
+r1OmUE57TvnvOZ7AISg9a4rAVOHRll42PDCxVE1DuJrMPw6TlYj8ZWZDlPSJee2c8NFS6pnMhOHz
+43hvo9b06+n0QWU9XvZOgO2rDVNGN2ZmAiLz/gR2nuwMaXPS062UcpZqAwgcOvsKZX4yvn8/3Xlu
+kkqxYbPXHIO1YcOxgbvPxDxQT91DvpWCQufzbIGoEOuo5ElgZHyEAcxNO5K9UHlKvizB6nMU4OJs
+yIU1pc9JQGz+ZpH4rWg8HfLv8wfyylIkCT7zppb37lt67r72/4sQQPIHwau0qTBJySGeuDNHUbTS
+G8mXFyiWrGPxmvMmfMVAjQ65tEwXBwGYEObaslOD5VpkqQCo2vB2x+S9lWvKGZiwoBfG5y+LoDFG
+qntlf3mrnHz4FXSkdtBv57SOldl+xi+97B+Ob/5x+2AmExC0z9Enat65PvwLXkDugN2sVcjMfKTy
+hSXCop6uuXpdg8R40406FiFoqZ+YIFdRibTyHJkpkqg5HKBoTFESSglgb8Rb9gZmCfUIjbMDMSsI
+WrB1lUcgxOZn+oEHrxHXd2vUVwBD6LxqZrPkVGg0SZVDaC/1x5EgawSuqwBugDU8R+cQvh0vQjzP
+qz1aOxkHFokPFh2fb4VNVnpUhi24
--- /dev/null
+type: account
+authority-id: testrootorg
+account-id: developer1
+display-name: Developer 1
+timestamp: 2016-08-11T18:46:02+02:00
+username: developer1
+validation: unproven
+sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+
+AcLBUgQAAQoABgUCV6yrygAAf0IQAFhUje+lWt6c2bmjUg17vo3tdowkpNYvXLma0ajCphULCmGg
+3qtS8sVCJa1rFXOeKr3whFc/iheFbZJyQh2lsIQsRE0Z587uxFLbs5ua0FU0yzU7va03PdfBIK9o
+BEC8uXdCx3yFlijnDibC3D6daqB/PkUUM+WT5ypvp4b4l/IY6Vf0s2p5IzYCToBcXzdXOuL6e8t8
+d2kC2q1P5WX+fK20UjSeGHSanR6sfr4Tw+FNgv/MtmaBAkhAbIHMKTOZaKd7sEjT0QLJVYRdWlTp
+dSpaJRqzTE2v7Ql7RjJtFO8+QKnjzNGMbRYj9yX9meBBeT+iDTqH4UrvyRBOmLlKVAkt8mXqjwWj
+IfuA+ISWb8Qc/aah/DO6wONt7oAD6AkXXrCFtinHyQCutD5/XB63eCeSJAfvxD2Nbx9xEWjwu4nE
+6D6a5URpU4Kzo05jk/OnCnjMHYgW/grn9wRt4yXAXWkENQfcAYH6ZZV4VTJqA+2fAO7q9v5WryA4
+z1WcX7073ORAFCviiUFEoX/3eUfsyom5AF45OKxs8bm8jfRemnfUjRrPbpV+auBZ+DG7NDKFAOVv
+zKKivSNac125adAcC3Xg1783eUMpDStylIOxziVYDB0e6nR4ekiccnvLv5GpiEOG0ZGa4sUYI6+V
+hWeNg9t8ydjCleMJcHt5WrgcBQuw
--- /dev/null
+type: account-key
+authority-id: testrootorg
+public-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+account-id: developer1
+name: default
+since: 2016-08-19T15:49:45+02:00
+body-length: 717
+sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+
+AcbBTQRWhcGAARAAtJGIguK7FhSyRxL/6jvdy0zAgGCjC1xVNFzeF76p5G8BXNEEHZUHK+z8Gr2J
+inVrpvhJhllf5Ob2dIMH2YQbC9jE1kjbzvuauQGDqk6tNQm0i3KDeHCSPgVN+PFXPwKIiLrh66Po
+AC7OfR1rFUgCqu0jch0H6Nue0ynvEPiY4dPeXq7mCdpDr5QIAM41L+3hg0OdzvO8HMIGZQpdF6jP
+7fkkVMROYvHUOJ8kknpKE7FiaNNpH7jK1qNxOYhLeiioX0LYrdmTvdTWHrSKZc82ZmlDjpKc4hUx
+VtTXMAysw7CzIdREPom/vJklnKLvZt+Wk5AEF5V5YKnuT3pY+fjVMZ56GtTEeO/Er/oLk/n2xUK5
+fD5DAyW/9z0ygzwTbY5IuWXyDfYneL4nXwWOEgg37Z4+8mTH+ftTz2dl1x1KIlIR2xo0kxf9t8K+
+jlr13vwF1+QReMCSUycUsZ2Eep5XhjI+LG7G1bMSGqodZTIOXLkIy6+3iJ8Z/feIHlJ0ELBDyFbl
+Yy04Sf9LI148vJMsYenonkoWejWdMi8iCUTeaZydHJEUBU/RbNFLjCWa6NIUe9bfZgLiOOZkps54
++/AL078ri/tGjo/5UGvezSmwrEoWJyqrJt2M69N2oVDLJcHeo2bUYPtFC2Kfb2je58JrJ+llifdg
+rAsxbnHXiXyVimUAEQEAAQ==
+
+AcLBXAQAAQoABgUCV9AFCQAKCRDdoJRfvd5vjf8OD/4nfa6dj+39OyxfBJYXCUgFj2qCPYUm66j+
+bwNY6YD96ZP4QKx4+Vhqrmu2RWhUISqHhJH/SPKxfil9nocWE16knSgE4HFUnb2omMBIq+wU1ThV
+JFvxdWXt9KFMKBlYeKr1BOoXuPJlQGf8PnsTM1oCqrzAyfGAbSFmsVrqzf8ujyQf551f5RovdSLX
+dqEZX1tYiyoIY9FJNGL5A74Q8CC9V1yJEPtEcQDEovoa368fD2S3rvyBYtaHnQ3tkrvZgZCPe0lJ
+4FwtljgGuAALAQPcZOepJRnNP60/eeYJGZPlQwwjD7akxy/FrDIjbqW/HRxHcroyKHGnMSD+DX5D
+GPSxClb1ChI+RqyNZJzubIh11lnLmidYsonNjTYZRmMDPn/iIo2Goso8hYKoF8ztJeD42otdLrMN
+omV7qZvf2bHZ5ADUPsK468CNOPnjQYL0AC5D01C+QxrkPfK8aJQiK0/rYcgCouSetzJHl0JrC0Fv
+6IfwpPxXz0/KHqDwsC20yGhmyCS8RjqzTX1pkn/hPeHD7qNJBP+4oXfyGdaEu+vsiAWIkgasv3Um
+pePncPNiBxTBHJEyYeRK6w5uy2M9iUe/QLT/1cHEYgsQDDr2mSV8rdpV9pn5JcyfiCL0nhIMPbTU
+DcIT7mkb48ulFZpKyVYubC5yDPGfaK621eRS5UfO1Q==
--- /dev/null
+type: account-key
+authority-id: testrootorg
+public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+account-id: testrootorg
+name: test-store
+since: 2016-08-11T18:42:22+02:00
+body-length: 717
+sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
+
+AcbBTQRWhcGAARAAmJqmZvsS58INTs+UQ+jfo836vBS5tkU/hk7c0huZe3So2gc9kaJvjkjhZ6g0
+0/kGoidw3i2WkdMEp+JtvU9Ztfeu/Nn/OqkSc3Ap1KAmqL4OllPVII8H69w2zqvmo+PcqH0SvHAV
+EoOC2ToXP0wEHnAZsbVu56AKrwpHDppPEIvaS6glrsEX1AXpOeMHZLVRtfsBB6dlLXuula1UrSAL
+RFCEjXtqXqOto5Vo9C8p63lLBy19ifz4OriWAGBqZvFdItmo8VIPXSDdgMMHBlu2MSNJHVfo66yp
+buos5Qs6PlVARACLJgzgplI5sDbzXtVx5O9Q8YJjz4NfU0WOWYPWvmANXDeMNixWoWvuixYvsXaG
+x6mdD7Hh/gi8prkQmZ7gxW1MEOV9JThAqYjjs2ayGVD73EI2sKYxVwEg3iJToQ/cEz3O2U1HdmYj
+QfRDJiX3GEPBXXttDrbPM42SHElouldmJ+PkJDLdkGmA85xYUoEKHdEFIkjFStQcyO5CkyNZN7SH
+iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW
+o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B
+1/Rrn+9VP9Swhv8AEQEAAQ==
+
+AcLBXAQAAQoABgUCV866kwAKCRBMcZp594FxpHWHD/9AaZXqyT/Zsmq/VzmAMpd9JvCH4PHQKtAP
+bXfP2Dnpa2wk2wuzQuSWunR8NDRyVh/aNVeTEZ9dFm/B8LR+U2O4rsHmFSeicmsTmo9u/HouRdEU
+zeSc6cbAxMPpfNSjr5J+URLjGRT6oX5fEBmRPx/OC9pEIScMx7uKmTKEnuyMzLRNN/6HiGWKrFCo
+nJdKkwRXrkCHyXWAOv1GumT7NDuyFcjAqt/UdHliTZkDBImKOsBmBVXMUjg7HCSS2uq/5WjStJ+B
+JHQ4GSsXBvVINs6BncNWcvV6mCQ73D57MzGhqo997Zb4tSrn7UNGWK7GLCzV3e/pFlG7pw6HbgnQ
++rxU2Oj/TPVw0tcnUiRl2ttKpm+nua0Cl+MD+Gx0KXLAVp0ZGOQ9yGyP9AePFzcOR8SlRIgxi0EI
+iJkSeYilqoKo3AJhnICRiqvAca2TGJoiJUryEgZ8jbTOElfaF2p+y0xvXGlWbKZm1gzGyvFM5fV5
+hJTlp/am+2uVn6U8wPACir4PrbuXYo7L4MIXww2OEO0ruBIaLARbc5IutSWmw6AEYQUxtsa9bdHV
+Zin7LGbEj6lZm8GycWQwh4B6Vnt6dJRIyPc/9G7uM8Ds/2Wa7+yAxhiPqm8DwlbOYh1npw4X4TLD
+IMGnTv5N3zllI+Xz4rqJzNTzEbvOIcrqWxCedQe79A==
--- /dev/null
+#!bash
+bootenv() {
+ if [ $# -eq 0 ]; then
+ if command -v grub-editenv >/dev/null; then
+ grub-editenv list
+ else
+ fw_printenv
+ fi
+ else
+ if command -v grub-editenv >/dev/null; then
+ grub-editenv list | grep "^$1"
+ else
+ fw_printenv "$1"
+ fi | sed "s/^${1}=//"
+ fi
+}
+
+# unset the given var from boot configuration
+bootenv_unset() {
+ local var="$1"
+
+ if command -v grub-editenv >/dev/null; then
+ grub-editenv /boot/grub/grubenv unset "$var"
+ else
+ fw_setenv "$var"
+ fi
+}
--- /dev/null
+#!/bin/sh
+set -ex
+
+INSTANCE_IP="${1:-localhost}"
+INSTANCE_PORT="${2:-8022}"
+USER="${3:-$(whoami)}"
+
+execute_remote(){
+ ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p $INSTANCE_PORT $USER@$INSTANCE_IP "$@"
+}
+
+execute_remote "sudo adduser --extrausers --quiet --disabled-password --gecos '' test"
+execute_remote "echo test:ubuntu | sudo chpasswd"
+execute_remote "echo 'test ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/create-user-test"
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+var devPrivKey, _ = assertstest.ReadPrivKey(assertstest.DevKey)
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Fprintf(os.Stderr, "no listening address arg\n")
+ os.Exit(1)
+ }
+
+ l, err := net.Listen("tcp", os.Args[1])
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cannot listen: %v\n", err)
+ os.Exit(1)
+ }
+
+ s := &http.Server{Handler: http.HandlerFunc(handle)}
+ go s.Serve(l)
+
+ ch := make(chan os.Signal)
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+ <-ch
+
+ l.Close()
+}
+
+func internalError(w http.ResponseWriter, msg string, a ...interface{}) {
+ http.Error(w, fmt.Sprintf(msg, a...), http.StatusInternalServerError)
+}
+
+func badRequestError(w http.ResponseWriter, msg string, a ...interface{}) {
+ http.Error(w, fmt.Sprintf(msg, a...), http.StatusBadRequest)
+}
+
+func handle(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/request-id":
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, fmt.Sprintf(`{"request-id": "REQ-ID"}`))
+ case "/serial":
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{})
+ if err != nil {
+ internalError(w, "cannot open signing db: %v", err)
+ return
+ }
+ err = db.ImportKey(devPrivKey)
+ if err != nil {
+ internalError(w, "cannot import signing key: %v", err)
+ return
+ }
+
+ b, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ internalError(w, "cannot read request: %v", err)
+ return
+ }
+
+ a, err := asserts.Decode(b)
+ if err != nil {
+ badRequestError(w, "cannot decode request: %v", err)
+ return
+ }
+
+ serialReq, ok := a.(*asserts.SerialRequest)
+ if !ok {
+ badRequestError(w, "request is not a serial-request")
+ return
+
+ }
+
+ err = asserts.SignatureCheck(serialReq, serialReq.DeviceKey())
+ if err != nil {
+ badRequestError(w, "bad serial-request: %v", err)
+ return
+ }
+
+ serialStr := "7777"
+ if r.Header.Get("X-Use-Proposed") == "yes" {
+ // use proposed serial
+ serialStr = serialReq.Serial()
+ }
+
+ serial, err := db.Sign(asserts.SerialType, map[string]interface{}{
+ "authority-id": "developer1",
+ "brand-id": "developer1",
+ "model": serialReq.Model(),
+ "serial": serialStr,
+ "device-key": serialReq.HeaderString("device-key"),
+ "device-key-sha3-384": serialReq.SignKeyID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, serialReq.Body(), devPrivKey.PublicKey().ID())
+ if err != nil {
+ internalError(w, "cannot sign serial: %v", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", asserts.MediaType)
+ w.WriteHeader(http.StatusOK)
+ w.Write(asserts.Encode(serial))
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/tests/lib/fakestore/refresh"
+ "github.com/snapcore/snapd/tests/lib/fakestore/store"
+)
+
+var (
+ start = flag.Bool("start", false, "Start the store service")
+ assertFallback = flag.Bool("assert-fallback", false, "Fallback to the main online store for missing assertions")
+ topDir = flag.String("dir", "", "Directory to be used by the store to keep and serve snaps, <dir>/asserts is used for assertions")
+ makeRefreshable = flag.String("make-refreshable", "", "List of snaps with new versions separated by commas")
+ addr = flag.String("addr", "localhost:11028", "Store address")
+ https_proxy = flag.String("https-proxy", "", "HTTPS proxy address")
+ http_proxy = flag.String("http-proxy", "", "HTTP proxy address")
+)
+
+func main() {
+ if err := logger.SimpleSetup(); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to activate logging: %v\n", err)
+ os.Exit(1)
+ }
+ logger.Debugf("fakestore starting")
+
+ if err := run(); err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func run() error {
+ flag.Parse()
+
+ if len(*https_proxy) > 0 {
+ os.Setenv("https_proxy", *https_proxy)
+ }
+
+ if len(*http_proxy) > 0 {
+ os.Setenv("http_proxy", *http_proxy)
+ }
+
+ if *start {
+ return runServer(*topDir, *addr, *assertFallback)
+ }
+
+ if *makeRefreshable != "" {
+ return runManage(*topDir, *makeRefreshable)
+ }
+
+ return fmt.Errorf("please specify either start or make-refreshable")
+}
+
+func runServer(topDir, addr string, assertFallback bool) error {
+ st := store.NewStore(topDir, addr, assertFallback)
+
+ if err := st.Start(); err != nil {
+ return err
+ }
+
+ ch := make(chan os.Signal)
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+ <-ch
+
+ return st.Stop()
+}
+
+func runManage(topDir, snaps string) error {
+ // setup fake new revisions of snaps for refresh
+ snapList := strings.Split(snaps, ",")
+ return refresh.MakeFakeRefreshForSnaps(snapList, topDir)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package refresh
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/asserts/systestkeys"
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+)
+
+func MakeFakeRefreshForSnaps(snaps []string, blobDir string) error {
+ storePrivKey, _ := assertstest.ReadPrivKey(systestkeys.TestStorePrivKey)
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ KeypairManager: asserts.NewMemoryKeypairManager(),
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: sysdb.Trusted(),
+ })
+ if err != nil {
+ return err
+ }
+ // for signing
+ db.ImportKey(storePrivKey)
+
+ var cliConfig client.Config
+ cli := client.New(&cliConfig)
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ headers := make(map[string]string)
+ for i, k := range ref.Type.PrimaryKey {
+ headers[k] = ref.PrimaryKey[i]
+ }
+ as, err := cli.Known(ref.Type.Name, headers)
+ if err != nil {
+ return nil, err
+ }
+ switch len(as) {
+ case 1:
+ return as[0], nil
+ case 0:
+ return nil, asserts.ErrNotFound
+ default:
+ panic(fmt.Sprintf("multiple assertions when retrieving by primary key: %v", ref))
+ }
+ }
+
+ save := func(a asserts.Assertion) error {
+ err := db.Add(a)
+ if err != nil {
+ if _, ok := err.(*asserts.RevisionError); !ok {
+ return err
+ }
+ }
+ return writeAssert(a, blobDir)
+ }
+
+ f := asserts.NewFetcher(db, retrieve, save)
+
+ for _, snap := range snaps {
+ if err := makeFakeRefreshForSnap(snap, blobDir, db, f); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func writeAssert(a asserts.Assertion, targetDir string) error {
+ ref := a.Ref()
+ fn := fmt.Sprintf("%s.%s", strings.Join(ref.PrimaryKey, ","), ref.Type.Name)
+ return ioutil.WriteFile(filepath.Join(targetDir, "asserts", fn), asserts.Encode(a), 0644)
+}
+
+func makeFakeRefreshForSnap(snap, targetDir string, db *asserts.Database, f asserts.Fetcher) error {
+ // make a fake update snap in /var/tmp (which is not a tempfs)
+ fakeUpdateDir, err := ioutil.TempDir("/var/tmp", "snap-build-")
+ if err != nil {
+ return fmt.Errorf("creating tmp for fake update: %v", err)
+ }
+ // ensure the "." of the squashfs has sane owner/permissions
+ err = exec.Command("sudo", "chown", "root:root", fakeUpdateDir).Run()
+ if err != nil {
+ return fmt.Errorf("changing owner of fake update dir: %v", err)
+ }
+ err = exec.Command("sudo", "chmod", "0755", fakeUpdateDir).Run()
+ if err != nil {
+ return fmt.Errorf("changing permissions of fake update dir: %v", err)
+ }
+ defer exec.Command("sudo", "rm", "-rf", fakeUpdateDir)
+
+ origInfo, err := copySnap(snap, fakeUpdateDir)
+ if err != nil {
+ return fmt.Errorf("copying snap: %v", err)
+ }
+
+ err = copySnapAsserts(origInfo, f)
+ if err != nil {
+ return fmt.Errorf("copying asserts: %v", err)
+ }
+
+ // fake new version
+ err = exec.Command("sudo", "sed", "-i", `s/version:\(.*\)/version:\1+fake1/`, filepath.Join(fakeUpdateDir, "meta/snap.yaml")).Run()
+ if err != nil {
+ return fmt.Errorf("changing fake snap version: %v", err)
+ }
+
+ newInfo, err := buildSnap(fakeUpdateDir, targetDir)
+ if err != nil {
+ return err
+ }
+
+ // new test-signed snap-revision
+ err = makeNewSnapRevision(origInfo, newInfo, targetDir, db)
+ if err != nil {
+ return fmt.Errorf("making new snap-revision: %v", err)
+ }
+
+ return nil
+}
+
+type info struct {
+ revision string
+ digest string
+ size uint64
+}
+
+func copySnap(snapName, targetDir string) (*info, error) {
+ baseDir := filepath.Join(dirs.SnapMountDir, snapName)
+ if _, err := os.Stat(baseDir); err != nil {
+ return nil, err
+ }
+ sourceDir := filepath.Join(baseDir, "current")
+ files, err := filepath.Glob(filepath.Join(sourceDir, "*"))
+ if err != nil {
+ return nil, err
+ }
+
+ revnoDir, err := filepath.EvalSymlinks(sourceDir)
+ if err != nil {
+ return nil, err
+ }
+ origRevision := filepath.Base(revnoDir)
+
+ for _, m := range files {
+ if err = exec.Command("sudo", "cp", "-a", m, targetDir).Run(); err != nil {
+ return nil, err
+
+ }
+ }
+
+ rev, err := snap.ParseRevision(origRevision)
+ if err != nil {
+ return nil, err
+ }
+
+ place := snap.MinimalPlaceInfo(snapName, rev)
+ origDigest, origSize, err := asserts.SnapFileSHA3_384(place.MountFile())
+ if err != nil {
+ return nil, err
+ }
+
+ return &info{revision: origRevision, size: origSize, digest: origDigest}, nil
+}
+
+func buildSnap(snapDir, targetDir string) (*info, error) {
+ // build in /var/tmp (which is not a tempfs)
+ cmd := exec.Command("snapbuild", snapDir, targetDir)
+ cmd.Env = append(os.Environ(), "TMPDIR=/var/tmp")
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return nil, fmt.Errorf("building fake snap: %v, output: %s", err, output)
+ }
+ out := strings.TrimSpace(string(output))
+ if !strings.HasPrefix(out, "built: ") {
+ return nil, fmt.Errorf("building fake snap got unexpected output: %s", output)
+ }
+ fn := out[len("built: "):]
+
+ newDigest, size, err := asserts.SnapFileSHA3_384(fn)
+ if err != nil {
+ return nil, err
+ }
+
+ return &info{digest: newDigest, size: size}, nil
+}
+
+func copySnapAsserts(info *info, f asserts.Fetcher) error {
+ return snapasserts.FetchSnapAssertions(f, info.digest)
+}
+
+func makeNewSnapRevision(orig, new *info, targetDir string, db *asserts.Database) error {
+ a, err := db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": orig.digest,
+ })
+ if err != nil {
+ return err
+ }
+ origSnapRev := a.(*asserts.SnapRevision)
+
+ headers := map[string]interface{}{
+ "authority-id": "testrootorg",
+ "snap-sha3-384": new.digest,
+ "snap-id": origSnapRev.SnapID(),
+ "snap-size": fmt.Sprintf("%d", new.size),
+ "snap-revision": fmt.Sprintf("%d", origSnapRev.SnapRevision()+1),
+ "developer-id": origSnapRev.DeveloperID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ a, err = db.Sign(asserts.SnapRevisionType, headers, nil, systestkeys.TestStoreKeyID)
+ if err != nil {
+ return err
+ }
+
+ return writeAssert(a, targetDir)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "gopkg.in/tylerb/graceful.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/asserts/systestkeys"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
+)
+
+func rootEndpoint(w http.ResponseWriter, req *http.Request) {
+ w.WriteHeader(418)
+ fmt.Fprintf(w, "I'm a teapot")
+}
+
+func hexify(in string) string {
+ bs, err := base64.RawURLEncoding.DecodeString(in)
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("%x", bs)
+}
+
+// Store is our snappy software store implementation
+type Store struct {
+ url string
+ blobDir string
+ assertDir string
+
+ assertFallback bool
+ fallback *store.Store
+
+ srv *graceful.Server
+}
+
+// NewStore creates a new store server serving snaps from the given top directory and assertions from topDir/asserts. If assertFallback is true missing assertions are looked up in the main online store.
+func NewStore(topDir, addr string, assertFallback bool) *Store {
+ mux := http.NewServeMux()
+ var sto *store.Store
+ if assertFallback {
+ store.SetUserAgentFromVersion("unknown", "fakestore")
+ sto = store.New(nil, nil)
+ }
+ store := &Store{
+ blobDir: topDir,
+ assertDir: filepath.Join(topDir, "asserts"),
+
+ assertFallback: assertFallback,
+ fallback: sto,
+
+ url: fmt.Sprintf("http://%s", addr),
+ srv: &graceful.Server{
+ Timeout: 2 * time.Second,
+
+ Server: &http.Server{
+ Addr: addr,
+ Handler: mux,
+ },
+ },
+ }
+
+ mux.HandleFunc("/", rootEndpoint)
+ mux.HandleFunc("/search", store.searchEndpoint)
+ mux.HandleFunc("/snaps/details/", store.detailsEndpoint)
+ mux.HandleFunc("/snaps/metadata", store.bulkEndpoint)
+ mux.Handle("/download/", http.StripPrefix("/download/", http.FileServer(http.Dir(topDir))))
+ mux.HandleFunc("/assertions/", store.assertionsEndpoint)
+
+ return store
+}
+
+// URL returns the base-url that the store is listening on
+func (s *Store) URL() string {
+ return s.url
+}
+
+func (s *Store) SnapsDir() string {
+ return s.blobDir
+}
+
+// Start listening
+func (s *Store) Start() error {
+ l, err := net.Listen("tcp", s.srv.Addr)
+ if err != nil {
+ return err
+ }
+
+ go s.srv.Serve(l)
+ return nil
+}
+
+// Stop stops the server
+func (s *Store) Stop() error {
+ timeoutTime := 2000 * time.Millisecond
+ s.srv.Stop(timeoutTime / 2)
+
+ select {
+ case <-s.srv.StopChan():
+ case <-time.After(timeoutTime):
+ return fmt.Errorf("store failed to stop after %s", timeoutTime)
+ }
+
+ return nil
+}
+
+var (
+ defaultDeveloper = "canonical"
+ defaultDeveloperID = "canonical"
+ defaultRevision = 424242
+)
+
+func makeRevision(info *snap.Info) int {
+ // TODO: This is a hack to ensure we have higher
+ // revisions here than locally. The fake
+ // snaps get versions like
+ // "1.0+fake1+fake1+fake1"
+ // so we can use this for now to generate
+ // fake revisions. However in the longer
+ // term we should read the real revision
+ // of the snap, increment and add a ".aux"
+ // file to the download directory of the
+ // store that contains the revision and the
+ // developer. The fake-store can then read
+ // that file when sending the reply.
+ n := strings.Count(info.Version, "+fake") + 1
+ return n * defaultRevision
+}
+
+type essentialInfo struct {
+ Name string
+ SnapID string
+ DeveloperID string
+ DevelName string
+ Revision int
+ Version string
+ Size uint64
+ Digest string
+}
+
+var errInfo = errors.New("cannot get info")
+
+func snapEssentialInfo(w http.ResponseWriter, fn, snapID string, bs asserts.Backstore) (*essentialInfo, error) {
+ snapFile, err := snap.Open(fn)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot read: %v: %v", fn, err), http.StatusBadRequest)
+ return nil, errInfo
+ }
+
+ info, err := snap.ReadInfoFromSnapFile(snapFile, nil)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("can get info for: %v: %v", fn, err), http.StatusBadRequest)
+ return nil, errInfo
+ }
+
+ snapDigest, size, err := asserts.SnapFileSHA3_384(fn)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("can get digest for: %v: %v", fn, err), http.StatusBadRequest)
+ return nil, errInfo
+ }
+
+ snapRev, devAcct, err := findSnapRevision(snapDigest, bs)
+ if err != nil && err != asserts.ErrNotFound {
+ http.Error(w, fmt.Sprintf("can get info for: %v: %v", fn, err), http.StatusBadRequest)
+ return nil, errInfo
+ }
+
+ var devel, develID string
+ var revision int
+ if snapRev != nil {
+ snapID = snapRev.SnapID()
+ develID = snapRev.DeveloperID()
+ devel = devAcct.Username()
+ revision = snapRev.SnapRevision()
+ } else {
+ // XXX: fallback until we are always assertion based
+ develID = defaultDeveloperID
+ devel = defaultDeveloper
+ revision = makeRevision(info)
+ }
+
+ return &essentialInfo{
+ Name: info.Name(),
+ SnapID: snapID,
+ DeveloperID: develID,
+ DevelName: devel,
+ Revision: revision,
+ Version: info.Version,
+ Digest: snapDigest,
+ Size: size,
+ }, nil
+}
+
+type searchPayloadJSON struct {
+ Packages []detailsReplyJSON `json:"clickindex:package"`
+}
+
+type searchReplyJSON struct {
+ Payload searchPayloadJSON `json:"_embedded"`
+}
+
+type detailsReplyJSON struct {
+ SnapID string `json:"snap_id"`
+ PackageName string `json:"package_name"`
+ Developer string `json:"origin"`
+ DeveloperID string `json:"developer_id"`
+ AnonDownloadURL string `json:"anon_download_url"`
+ DownloadURL string `json:"download_url"`
+ Version string `json:"version"`
+ Revision int `json:"revision"`
+ DownloadDigest string `json:"download_sha3_384"`
+}
+
+func (s *Store) searchEndpoint(w http.ResponseWriter, req *http.Request) {
+ w.WriteHeader(501)
+ fmt.Fprintf(w, "search not implemented")
+}
+
+func (s *Store) detailsEndpoint(w http.ResponseWriter, req *http.Request) {
+ pkg := strings.TrimPrefix(req.URL.Path, "/snaps/details/")
+ if pkg == req.URL.Path {
+ panic("how?")
+ }
+
+ bs, err := s.collectAssertions()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting assertions: %v", err), http.StatusInternalServerError)
+ return
+ }
+ snaps, err := s.collectSnaps()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting snaps: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ fn, ok := snaps[pkg]
+ if !ok {
+ http.NotFound(w, req)
+ return
+ }
+
+ essInfo, err := snapEssentialInfo(w, fn, "", bs)
+ if essInfo == nil {
+ if err != errInfo {
+ panic(err)
+ }
+ return
+ }
+
+ details := detailsReplyJSON{
+ SnapID: essInfo.SnapID,
+ PackageName: essInfo.Name,
+ Developer: essInfo.DevelName,
+ DeveloperID: essInfo.DeveloperID,
+ AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.URL(), filepath.Base(fn)),
+ DownloadURL: fmt.Sprintf("%s/download/%s", s.URL(), filepath.Base(fn)),
+ Version: essInfo.Version,
+ Revision: essInfo.Revision,
+ DownloadDigest: hexify(essInfo.Digest),
+ }
+
+ // use indent because this is a development tool, output
+ // should look nice
+ out, err := json.MarshalIndent(details, "", " ")
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot marshal: %v: %v", details, err), http.StatusBadRequest)
+ return
+ }
+ w.Write(out)
+}
+
+func (s *Store) collectSnaps() (map[string]string, error) {
+ snapFns, err := filepath.Glob(filepath.Join(s.blobDir, "*.snap"))
+ if err != nil {
+ return nil, err
+ }
+
+ snaps := map[string]string{}
+
+ for _, fn := range snapFns {
+ snapFile, err := snap.Open(fn)
+ if err != nil {
+ return nil, err
+ }
+ info, err := snap.ReadInfoFromSnapFile(snapFile, nil)
+ if err != nil {
+ return nil, err
+ }
+ snaps[info.Name()] = fn
+ }
+
+ return snaps, err
+}
+
+type candidateSnap struct {
+ SnapID string `json:"snap_id"`
+}
+
+type bulkReqJSON struct {
+ CandidateSnaps []candidateSnap `json:"snaps"`
+ Fields []string `json:"fields"`
+}
+
+type payload struct {
+ Packages []detailsReplyJSON `json:"clickindex:package"`
+}
+
+type bulkReplyJSON struct {
+ Payload payload `json:"_embedded"`
+}
+
+var someSnapIDtoName = map[string]string{
+ "b8X2psL1ryVrPt5WEmpYiqfr5emixTd7": "ubuntu-core",
+ "99T7MUlRhtI3U0QFgl5mXXESAiSwt776": "core",
+ "bul8uZn9U3Ll4ke6BMqvNVEZjuJCSQvO": "canonical-pc",
+ "SkKeDk2PRgBrX89DdgULk3pyY5DJo6Jk": "canonical-pc-linux",
+ "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw": "test-snapd-tools",
+ "Wcs8QL2iRQMjsPYQ4qz4V1uOlElZ1ZOb": "test-snapd-python-webserver",
+ "DVvhXhpa9oJjcm0rnxfxftH1oo5vTW1M": "test-snapd-go-webserver",
+}
+
+func (s *Store) bulkEndpoint(w http.ResponseWriter, req *http.Request) {
+ var pkgs bulkReqJSON
+ var replyData bulkReplyJSON
+
+ decoder := json.NewDecoder(req.Body)
+ if err := decoder.Decode(&pkgs); err != nil {
+ http.Error(w, fmt.Sprintf("cannot decode request body: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ bs, err := s.collectAssertions()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting assertions: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ snapIDtoName, err := addSnapIDs(bs, someSnapIDtoName)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting snapIDs: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ snaps, err := s.collectSnaps()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting snaps: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // check if we have downloadable snap of the given SnapID
+ for _, pkg := range pkgs.CandidateSnaps {
+
+ name := snapIDtoName[pkg.SnapID]
+ if name == "" {
+ http.Error(w, fmt.Sprintf("unknown snapid: %q", pkg.SnapID), http.StatusBadRequest)
+ return
+ }
+
+ if fn, ok := snaps[name]; ok {
+ essInfo, err := snapEssentialInfo(w, fn, pkg.SnapID, bs)
+ if essInfo == nil {
+ if err != errInfo {
+ panic(err)
+ }
+ return
+ }
+
+ replyData.Payload.Packages = append(replyData.Payload.Packages, detailsReplyJSON{
+ SnapID: essInfo.SnapID,
+ PackageName: essInfo.Name,
+ Developer: essInfo.DevelName,
+ DeveloperID: essInfo.DeveloperID,
+ DownloadURL: fmt.Sprintf("%s/download/%s", s.URL(), filepath.Base(fn)),
+ AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.URL(), filepath.Base(fn)),
+ Version: essInfo.Version,
+ Revision: essInfo.Revision,
+ DownloadDigest: hexify(essInfo.Digest),
+ })
+ }
+ }
+
+ // use indent because this is a development tool, output
+ // should look nice
+ out, err := json.MarshalIndent(replyData, "", " ")
+ if err != nil {
+ http.Error(w, fmt.Sprintf("can marshal: %v: %v", replyData, err), http.StatusBadRequest)
+ return
+ }
+ w.Write(out)
+
+}
+
+func (s *Store) collectAssertions() (asserts.Backstore, error) {
+ bs := asserts.NewMemoryBackstore()
+
+ add := func(a asserts.Assertion) {
+ bs.Put(a.Type(), a)
+ }
+
+ for _, t := range sysdb.Trusted() {
+ add(t)
+ }
+ add(systestkeys.TestRootAccount)
+ add(systestkeys.TestRootAccountKey)
+ add(systestkeys.TestStoreAccountKey)
+
+ aFiles, err := filepath.Glob(filepath.Join(s.assertDir, "*"))
+ if err != nil {
+ return nil, err
+ }
+
+ for _, fn := range aFiles {
+ b, err := ioutil.ReadFile(fn)
+ if err != nil {
+ return nil, err
+ }
+
+ a, err := asserts.Decode(b)
+ if err != nil {
+ return nil, err
+ }
+
+ add(a)
+ }
+
+ return bs, nil
+}
+
+func isAssertNotFound(err error) bool {
+ if err == asserts.ErrNotFound {
+ return true
+ }
+ if _, ok := err.(*store.AssertionNotFoundError); ok {
+ return true
+ }
+ return false
+}
+
+func (s *Store) retrieveAssertion(bs asserts.Backstore, assertType *asserts.AssertionType, primaryKey []string) (asserts.Assertion, error) {
+ a, err := bs.Get(assertType, primaryKey, assertType.MaxSupportedFormat())
+ if err == asserts.ErrNotFound && s.assertFallback {
+ return s.fallback.Assertion(assertType, primaryKey, nil)
+ }
+ return a, err
+}
+
+func (s *Store) assertionsEndpoint(w http.ResponseWriter, req *http.Request) {
+ assertPath := strings.TrimPrefix(req.URL.Path, "/assertions/")
+
+ bs, err := s.collectAssertions()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting assertions: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ comps := strings.Split(assertPath, "/")
+
+ if len(comps) == 0 {
+ http.Error(w, "missing assertion type", http.StatusBadRequest)
+ return
+
+ }
+
+ typ := asserts.Type(comps[0])
+ if typ == nil {
+ http.Error(w, fmt.Sprintf("unknown assertion type: %s", comps[0]), http.StatusBadRequest)
+ return
+ }
+
+ if len(typ.PrimaryKey) != len(comps)-1 {
+ http.Error(w, fmt.Sprintf("wrong primary key length: %v", comps), http.StatusBadRequest)
+ return
+ }
+
+ a, err := s.retrieveAssertion(bs, typ, comps[1:])
+ if isAssertNotFound(err) {
+ w.Header().Set("Content-Type", "application/problem+json")
+ w.WriteHeader(404)
+ w.Write([]byte(`{"status": 404}`))
+ return
+ }
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot retrieve assertion %v: %v", comps, err), http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", asserts.MediaType)
+ w.WriteHeader(http.StatusOK)
+ w.Write(asserts.Encode(a))
+}
+
+func addSnapIDs(bs asserts.Backstore, initial map[string]string) (map[string]string, error) {
+ m := make(map[string]string)
+ for id, name := range initial {
+ m[id] = name
+ }
+
+ hit := func(a asserts.Assertion) {
+ decl := a.(*asserts.SnapDeclaration)
+ m[decl.SnapID()] = decl.SnapName()
+ }
+
+ err := bs.Search(asserts.SnapDeclarationType, nil, hit, asserts.SnapDeclarationType.MaxSupportedFormat())
+ if err != nil {
+ return nil, err
+ }
+
+ return m, nil
+}
+
+func findSnapRevision(snapDigest string, bs asserts.Backstore) (*asserts.SnapRevision, *asserts.Account, error) {
+ a, err := bs.Get(asserts.SnapRevisionType, []string{snapDigest}, asserts.SnapRevisionType.MaxSupportedFormat())
+ if err != nil {
+ return nil, nil, err
+ }
+ snapRev := a.(*asserts.SnapRevision)
+
+ a, err = bs.Get(asserts.AccountType, []string{snapRev.DeveloperID()}, asserts.AccountType.MaxSupportedFormat())
+ if err != nil {
+ return nil, nil, err
+ }
+ devAcct := a.(*asserts.Account)
+
+ return snapRev, devAcct, nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package store
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "testing"
+ "text/template"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/systestkeys"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap/snaptest"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type storeTestSuite struct {
+ client *http.Client
+ store *Store
+}
+
+var _ = Suite(&storeTestSuite{})
+
+var defaultAddr = "localhost:23321"
+
+func getSha(fn string) string {
+ snapDigest, _, err := asserts.SnapFileSHA3_384(fn)
+ if err != nil {
+ panic(err)
+ }
+ return hexify(snapDigest)
+}
+
+func (s *storeTestSuite) SetUpTest(c *C) {
+ topdir := c.MkDir()
+ err := os.Mkdir(filepath.Join(topdir, "asserts"), 0755)
+ c.Assert(err, IsNil)
+ s.store = NewStore(topdir, defaultAddr, false)
+ err = s.store.Start()
+ c.Assert(err, IsNil)
+
+ transport := &http.Transport{}
+ s.client = &http.Client{
+ Transport: transport,
+ }
+}
+
+func (s *storeTestSuite) TearDownTest(c *C) {
+ s.client.Transport.(*http.Transport).CloseIdleConnections()
+ err := s.store.Stop()
+ c.Assert(err, IsNil)
+}
+
+// StoreGet gets the given from the store
+func (s *storeTestSuite) StoreGet(path string) (*http.Response, error) {
+ return s.client.Get(s.store.URL() + path)
+}
+
+func (s *storeTestSuite) StorePostJSON(path string, content []byte) (*http.Response, error) {
+ r := bytes.NewReader(content)
+ return s.client.Post(s.store.URL()+path, "application/json", r)
+}
+
+func (s *storeTestSuite) TestStoreURL(c *C) {
+ c.Assert(s.store.URL(), Equals, "http://"+defaultAddr)
+}
+
+func (s *storeTestSuite) TestTrivialGetWorks(c *C) {
+ resp, err := s.StoreGet("/")
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 418)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, "I'm a teapot")
+
+}
+
+func (s *storeTestSuite) TestSearchEndpoint(c *C) {
+ resp, err := s.StoreGet("/search")
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 501)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, "search not implemented")
+
+}
+
+func (s *storeTestSuite) TestDetailsEndpointWithAssertions(c *C) {
+ snapFn := s.makeTestSnap(c, "name: foo\nversion: 7")
+ s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 77)
+
+ resp, err := s.StoreGet(`/snaps/details/foo`)
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, fmt.Sprintf(`{
+ "snap_id": "xidididididididididididididididid",
+ "package_name": "foo",
+ "origin": "foo-devel",
+ "developer_id": "foo-devel-id",
+ "anon_download_url": "%s/download/foo_7_all.snap",
+ "download_url": "%s/download/foo_7_all.snap",
+ "version": "7",
+ "revision": 77,
+ "download_sha3_384": "%s"
+}`, s.store.URL(), s.store.URL(), getSha(snapFn)))
+}
+
+func (s *storeTestSuite) TestDetailsEndpoint(c *C) {
+ snapFn := s.makeTestSnap(c, "name: foo\nversion: 1")
+ resp, err := s.StoreGet(`/snaps/details/foo`)
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, fmt.Sprintf(`{
+ "snap_id": "",
+ "package_name": "foo",
+ "origin": "canonical",
+ "developer_id": "canonical",
+ "anon_download_url": "%s/download/foo_1_all.snap",
+ "download_url": "%s/download/foo_1_all.snap",
+ "version": "1",
+ "revision": 424242,
+ "download_sha3_384": "%s"
+}`, s.store.URL(), s.store.URL(), getSha(snapFn)))
+}
+
+func (s *storeTestSuite) TestBulkEndpoint(c *C) {
+ snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1")
+
+ // note that we send the test-snapd-tools snapID here
+ resp, err := s.StorePostJSON("/snaps/metadata", []byte(`{
+"snaps": [{"snap_id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","channel":"stable","revision":1}]
+}`))
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, fmt.Sprintf(`{
+ "_embedded": {
+ "clickindex:package": [
+ {
+ "snap_id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "package_name": "test-snapd-tools",
+ "origin": "canonical",
+ "developer_id": "canonical",
+ "anon_download_url": "%[1]s/download/test-snapd-tools_1_all.snap",
+ "download_url": "%[1]s/download/test-snapd-tools_1_all.snap",
+ "version": "1",
+ "revision": 424242,
+ "download_sha3_384": "%s"
+ }
+ ]
+ }
+}`, s.store.URL(), getSha(snapFn)))
+}
+
+func (s *storeTestSuite) TestBulkEndpointWithAssertions(c *C) {
+ snapFn := s.makeTestSnap(c, "name: foo\nversion: 10")
+ s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 99)
+
+ resp, err := s.StorePostJSON("/snaps/metadata", []byte(`{
+"snaps": [{"snap_id":"xidididididididididididididididid","channel":"stable","revision":1}]
+}`))
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, fmt.Sprintf(`{
+ "_embedded": {
+ "clickindex:package": [
+ {
+ "snap_id": "xidididididididididididididididid",
+ "package_name": "foo",
+ "origin": "foo-devel",
+ "developer_id": "foo-devel-id",
+ "anon_download_url": "%[1]s/download/foo_10_all.snap",
+ "download_url": "%[1]s/download/foo_10_all.snap",
+ "version": "10",
+ "revision": 99,
+ "download_sha3_384": "%s"
+ }
+ ]
+ }
+}`, s.store.URL(), getSha(snapFn)))
+}
+
+func (s *storeTestSuite) makeTestSnap(c *C, snapYamlContent string) string {
+ fn := snaptest.MakeTestSnapWithFiles(c, snapYamlContent, nil)
+ dst := filepath.Join(s.store.blobDir, filepath.Base(fn))
+ err := osutil.CopyFile(fn, dst, 0)
+ c.Assert(err, IsNil)
+ return dst
+}
+
+var (
+ tSnapDecl = template.Must(template.New("snap-decl").Parse(`type: snap-declaration
+authority-id: testrootorg
+series: 16
+snap-id: {{.SnapID}}
+publisher-id: {{.DeveloperID}}
+snap-name: {{.Name}}
+timestamp: 2016-08-19T19:19:19Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=
+`))
+ tSnapRev = template.Must(template.New("snap-rev").Parse(`type: snap-revision
+authority-id: testrootorg
+snap-sha3-384: {{.Digest}}
+developer-id: {{.DeveloperID}}
+snap-id: {{.SnapID}}
+snap-revision: {{.Revision}}
+snap-size: {{.Size}}
+timestamp: 2016-08-19T19:19:19Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=
+`))
+ tAccount = template.Must(template.New("acct").Parse(`type: account
+authority-id: testrootorg
+account-id: {{.DeveloperID}}
+display-name: {{.DevelName}} Dev
+username: {{.DevelName}}
+validation: unproven
+timestamp: 2016-08-19T19:19:19Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=
+`))
+)
+
+func (s *storeTestSuite) makeAssertions(c *C, snapFn, name, snapID, develName, develID string, revision int) {
+ dgst, size, err := asserts.SnapFileSHA3_384(snapFn)
+ c.Assert(err, IsNil)
+
+ info := essentialInfo{
+ Name: name,
+ SnapID: snapID,
+ DeveloperID: develID,
+ DevelName: develName,
+ Revision: revision,
+ Size: size,
+ Digest: dgst,
+ }
+
+ f, err := os.OpenFile(filepath.Join(s.store.assertDir, snapID+".fake.snap-declaration"), os.O_CREATE|os.O_WRONLY, 0644)
+ c.Assert(err, IsNil)
+ err = tSnapDecl.Execute(f, info)
+ c.Assert(err, IsNil)
+
+ f, err = os.OpenFile(filepath.Join(s.store.assertDir, dgst+".fake.snap-revision"), os.O_CREATE|os.O_WRONLY, 0644)
+ c.Assert(err, IsNil)
+ err = tSnapRev.Execute(f, info)
+ c.Assert(err, IsNil)
+
+ f, err = os.OpenFile(filepath.Join(s.store.assertDir, develID+".fake.account"), os.O_CREATE|os.O_WRONLY, 0644)
+ c.Assert(err, IsNil)
+ err = tAccount.Execute(f, info)
+ c.Assert(err, IsNil)
+}
+
+func (s *storeTestSuite) TestMakeTestSnap(c *C) {
+ snapFn := s.makeTestSnap(c, "name: foo\nversion: 1")
+ c.Assert(osutil.FileExists(snapFn), Equals, true)
+ c.Assert(snapFn, Equals, filepath.Join(s.store.blobDir, "foo_1_all.snap"))
+}
+
+func (s *storeTestSuite) TestCollectSnaps(c *C) {
+ s.makeTestSnap(c, "name: foo\nversion: 1")
+
+ snaps, err := s.store.collectSnaps()
+ c.Assert(err, IsNil)
+ c.Assert(snaps, DeepEquals, map[string]string{
+ "foo": filepath.Join(s.store.blobDir, "foo_1_all.snap"),
+ })
+}
+
+func (s *storeTestSuite) TestSnapDownloadByFullname(c *C) {
+ s.makeTestSnap(c, "name: foo\nversion: 1")
+
+ resp, err := s.StoreGet("/download/foo_1_all.snap")
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+}
+
+const (
+ exampleSnapRev = `type: snap-revision
+authority-id: canonical
+snap-sha3-384: QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL
+snap-id: snap-id-1
+snap-size: 999
+snap-revision: 36
+developer-id: developer1
+timestamp: 2016-08-19T19:19:19Z
+sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+AXNpZw=`
+)
+
+func (s *storeTestSuite) TestAssertionsEndpointPreloaded(c *C) {
+ // something preloaded
+ resp, err := s.StoreGet(`/assertions/account/testrootorg`)
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ c.Check(resp.Header.Get("Content-Type"), Equals, "application/x.ubuntu.assertion")
+
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(body), Equals, string(asserts.Encode(systestkeys.TestRootAccount)))
+}
+
+func (s *storeTestSuite) TestAssertionsEndpointFromAssertsDir(c *C) {
+ // something put in the assertion directory
+ a, err := asserts.Decode([]byte(exampleSnapRev))
+ c.Assert(err, IsNil)
+ rev := a.(*asserts.SnapRevision)
+
+ err = ioutil.WriteFile(filepath.Join(s.store.assertDir, "foo_36.snap-revision"), []byte(exampleSnapRev), 0655)
+ c.Assert(err, IsNil)
+
+ resp, err := s.StoreGet(`/assertions/snap-revision/` + rev.SnapSHA3_384())
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(body), Equals, exampleSnapRev)
+}
+
+func (s *storeTestSuite) TestAssertionsEndpointNotFound(c *C) {
+ // something not found
+ resp, err := s.StoreGet(`/assertions/account/not-an-account-id`)
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 404)
+
+ dec := json.NewDecoder(resp.Body)
+ var respObj map[string]interface{}
+ err = dec.Decode(&respObj)
+ c.Assert(err, IsNil)
+ c.Check(respObj["status"], Equals, float64(404))
+}
--- /dev/null
+#!/bin/sh
+echo "setup fake gpg pinentry environment"
+cat > /tmp/pinentry-fake <<'EOF'
+#!/bin/sh
+set -e
+echo "OK Pleased to meet you"
+while true; do
+ read line
+ case $line in
+ GETPIN)
+ echo "D pass"
+ echo "OK"
+ ;;
+ BYE)
+ exit 0
+ ;;
+ *)
+ echo "OK I'm not very smart"
+ ;;
+esac
+done
+EOF
+chmod +x /tmp/pinentry-fake
+mkdir -pm 0700 $HOME/.snap/gnupg/
+echo pinentry-program /tmp/pinentry-fake > $HOME/.snap/gnupg/gpg-agent.conf
--- /dev/null
+#!/bin/bash
+gadget_name=$(snap list | sed -n 's/^\(pc\|pi[23]\|dragonboard\) .*/\1/p')
+kernel_name=$gadget_name-kernel
+core_name=$(snap list | awk '/^(ubuntu-)?core / {print $1; exit}')
+
+if [ "$kernel_name" = "pi3-kernel" ]; then
+ kernel_name=pi2-kernel
+fi
--- /dev/null
+#!/bin/sh
+get_default_iface(){
+ echo "$(ip route get 8.8.8.8 | awk '{ print $5; exit }')"
+}
--- /dev/null
+NAME="Ubuntu Core"
+VERSION="16"
+ID=ubuntu-core
+PRETTY_NAME="Ubuntu Core 16"
+VERSION_ID="16""
+HOME_URL="http://www.snapcraft.io/"
+BUG_REPORT_URL="http://bugs.launchpad.net/snappy/"
--- /dev/null
+#!/bin/bash
+
+set -eux
+
+. $TESTSLIB/apt.sh
+
+update_core_snap_for_classic_reexec() {
+ # it is possible to disable this to test that snapd (the deb) works
+ # fine with whatever is in the core snap
+ if [ "$MODIFY_CORE_SNAP_FOR_REEXEC" != "1" ]; then
+ echo "Not modifying the core snap as requested via MODIFY_CORE_SNAP_FOR_REEXEC"
+ return
+ fi
+
+ # We want to use the in-tree snap/snapd/snap-exec/snapctl, because
+ # we re-exec by default.
+ # To accomplish that, we'll just unpack the core we just grabbed,
+ # shove the new snap-exec and snapctl in there, and repack it.
+
+ # First of all, unmount the core
+ core="$(readlink -f /snap/core/current || readlink -f /snap/ubuntu-core/current)"
+ snap="$(mount | grep " $core" | awk '{print $1}')"
+ umount --verbose "$core"
+
+ # Now unpack the core, inject the new snap-exec/snapctl into it
+ unsquashfs "$snap"
+ cp /usr/lib/snapd/snap-exec squashfs-root/usr/lib/snapd/
+ cp /usr/bin/snapctl squashfs-root/usr/bin/
+ # also add snap/snapd because we re-exec by default and want to test
+ # this version
+ cp /usr/lib/snapd/snapd squashfs-root/usr/lib/snapd/
+ cp /usr/lib/snapd/info squashfs-root/usr/lib/snapd/
+ cp /usr/bin/snap squashfs-root/usr/bin/snap
+
+ # repack, cheating to speed things up (4sec vs 1.5min)
+ mv "$snap" "${snap}.orig"
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ # trusty does not support -Xcompression-level 1
+ mksquashfs squashfs-root "$snap" -comp gzip
+ else
+ mksquashfs squashfs-root "$snap" -comp gzip -Xcompression-level 1
+ fi
+ rm -rf squashfs-root
+
+ # Now mount the new core snap
+ mount "$snap" "$core"
+
+ # Make sure we're running with the correct copied bits
+ for p in /usr/lib/snapd/snap-exec /usr/bin/snapctl /usr/lib/snapd/snapd /usr/bin/snap; do
+ if ! cmp ${p} ${core}${p}; then
+ echo "$p in tree and $p in core snap are unexpectedly not the same"
+ exit 1
+ fi
+ done
+}
+
+prepare_classic() {
+ apt_install_local ${GOPATH}/snap-confine*.deb ${GOPATH}/ubuntu-core-launcher_*.deb
+ apt_install_local ${GOPATH}/snapd_*.deb
+ if snap --version |MATCH unknown; then
+ echo "Package build incorrect, 'snap --version' mentions 'unknown'"
+ snap --version
+ apt-cache policy snapd
+ exit 1
+ fi
+ if /usr/lib/snapd/snap-confine --version | MATCH unknown; then
+ echo "Package build incorrect, 'snap-confine --version' mentions 'unknown'"
+ /usr/lib/snapd/snap-confine --version
+ apt-cache policy snap-confine
+ exit 1
+ fi
+
+ # Disable burst limit so resetting the state quickly doesn't create
+ # problems.
+ mkdir -p /etc/systemd/system/snapd.service.d
+ if [ -n "${SNAP_REEXEC:-}" ]; then
+ EXTRA_ENV="SNAP_REEXEC=$SNAP_REEXEC"
+ else
+ EXTRA_ENV=""
+ fi
+ cat <<EOF > /etc/systemd/system/snapd.service.d/local.conf
+[Unit]
+StartLimitInterval=0
+[Service]
+Environment=SNAPD_DEBUG_HTTP=7 SNAPPY_TESTING=1 $EXTRA_ENV
+EOF
+ mkdir -p /etc/systemd/system/snapd.socket.d
+ cat <<EOF > /etc/systemd/system/snapd.socket.d/local.conf
+[Unit]
+StartLimitInterval=0
+EOF
+
+ # Snapshot the state including core.
+ if [ ! -f $SPREAD_PATH/snapd-state.tar.gz ]; then
+ ! snap list | grep core || exit 1
+ # use parameterized core channel (defaults to edge) instead
+ # of a fixed one and close to stable in order to detect defects
+ # earlier
+ snap install --${CORE_CHANNEL} core
+ snap list | grep core
+
+ echo "Ensure that the grub-editenv list output is empty on classic"
+ output=$(grub-editenv list)
+ if [ -n "$output" ]; then
+ echo "Expected empty grub environment, got:"
+ echo "$output"
+ exit 1
+ fi
+
+ systemctl stop snapd.service snapd.socket
+
+ update_core_snap_for_classic_reexec
+
+ systemctl daemon-reload
+ mounts="$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')"
+ services="$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ')"
+ for unit in $services $mounts; do
+ systemctl stop $unit
+ done
+ tar czf $SPREAD_PATH/snapd-state.tar.gz /var/lib/snapd /snap /etc/systemd/system/snap-*core*.mount
+ systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/
+ for unit in $mounts $services; do
+ systemctl start $unit
+ done
+ systemctl start snapd.socket
+ fi
+}
+
+setup_reflash_magic() {
+ # install the stuff we need
+ apt-get install -y kpartx busybox-static
+ apt_install_local ${GOPATH}/snapd_*.deb ${GOPATH}/snap-confine_*.deb ${GOPATH}/ubuntu-core-launcher_*.deb
+ apt-get clean
+
+ snap install --${CORE_CHANNEL} core
+
+ # install ubuntu-image
+ snap install --devmode --edge ubuntu-image
+
+ # needs to be under /home because ubuntu-device-flash
+ # uses snap-confine and that will hide parts of the hostfs
+ IMAGE_HOME=/home/image
+ mkdir -p $IMAGE_HOME
+
+ # modify the core snap so that the current root-pw works there
+ # for spread to do the first login
+ UNPACKD="/tmp/core-snap"
+ unsquashfs -d $UNPACKD /var/lib/snapd/snaps/core_*.snap
+
+ # FIXME: netplan workaround
+ mkdir -p $UNPACKD/etc/netplan
+
+ # set root pw by concating root line from host and rest from core
+ want_pw="$(grep ^root /etc/shadow)"
+ echo "$want_pw" > /tmp/new-shadow
+ tail -n +2 /etc/shadow >> /tmp/new-shadow
+ cp -v /tmp/new-shadow $UNPACKD/etc/shadow
+ cp -v /etc/passwd $UNPACKD/etc/passwd
+
+ # ensure spread -reuse works in the core image as well
+ if [ -e /.spread.yaml ]; then
+ cp -av /.spread.yaml $UNPACKD
+ fi
+
+ # we need the test user in the image
+ # see the comment in spread.yaml about 12345
+ sed -i "s/^test.*$//" $UNPACKD/etc/{shadow,passwd}
+ chroot $UNPACKD addgroup --quiet --gid 12345 test
+ chroot $UNPACKD adduser --quiet --no-create-home --uid 12345 --gid 12345 --disabled-password --gecos '' test
+ echo 'test ALL=(ALL) NOPASSWD:ALL' >> $UNPACKD/etc/sudoers.d/99-test-user
+
+ echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' >> $UNPACKD/etc/sudoers.d/99-ubuntu-user
+
+ # modify sshd so that we can connect as root
+ sed -i 's/\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' $UNPACKD/etc/ssh/sshd_config
+
+ # FIXME: install would be better but we don't have dpkg on
+ # the image
+ # unpack our freshly build snapd into the new core snap
+ dpkg-deb -x ${SPREAD_PATH}/../snapd_*.deb $UNPACKD
+ dpkg-deb -x ${SPREAD_PATH}/../snap-confine_*.deb $UNPACKD
+
+ # add gpio and iio slots
+ cat >> $UNPACKD/meta/snap.yaml <<-EOF
+slots:
+ gpio-pin:
+ interface: gpio
+ number: 100
+ direction: out
+ iio0:
+ interface: iio
+ path: /dev/iio:device0
+EOF
+
+ # build new core snap for the image
+ snapbuild $UNPACKD $IMAGE_HOME
+
+ # FIXME: fetch directly once its in the assertion service
+ cat > $IMAGE_HOME/pc.model <<EOF
+type: model
+authority-id: canonical
+series: 16
+brand-id: canonical
+model: pc
+architecture: amd64
+gadget: pc
+kernel: pc-kernel
+timestamp: 2016-08-31T00:00:00.0Z
+sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
+
+AcLBXAQAAQoABgUCV8lRDQAKCRDgT5vottzAEg9GEACsSb+qXB34mwESsd7ns6VpM9BfAOOSstwB
+KJlWOlcJ39M7is/fO+dxRH4XsI7Td6BI1WEf5188sJuld8APUsTPn8tPYN3JB5CJ8Edkr6p78YUW
+f3Wo26USAE32ewjq9kHo6uBqIr4VixjTXfGUeDXc7tvKcduIMokSKjDLRHJRur1NC8LjkBn2ZPi8
+9d0BpJzr5y8wK0yFEyAhaS8H8LvL7VMjKG7/BkZcQ0a3jv69qh9jdmxnKDN2zcd1btRR1Giew3gw
+VJ8lNtfxQSWi+nYNEuzDqwKdffo9sVyCzBC+vEH3xYYk8NpRx2QgCSzDCPMoxaJgLwhAeWz6mHQp
+8EaGOsMZm7c85BXUcdJGEhZ5MpNGSzCb/ifgOKBB6zYzekiQh4TVLgi9Uk/acsLH75vNrI8Kwyl+
+r4Pahf///LbeWNwcEonaSV48S5fg3QqxEQeb42xcp6wPfRr7LN1LvQ9kRQTt42GDAlIva5HKlo0T
+cUb5A4zz3IlBn/KQ4BS/2sBcixrH97tHInef4oA8IrBiBDGnIv/s4qyZ+gB5fX8Ohnn/a5bUgU5u
+GmwRQ12Ix54YGJrzZocu1AiQINij4s6ZSoJAEJobI9VBK8WnV8PRmra6UJonV+qrJOiSKTJVCkAF
++RFartQL+pjF/H29FsyBkIEcPwhTslxWKUWajHsExw==
+EOF
+
+ # FIXME: how to test store updated of ubuntu-core with sideloaded snap?
+ IMAGE=all-snap-amd64.img
+
+ # ensure that ubuntu-image is using our test-build of snapd with the
+ # test keys and not the bundled version of usr/bin/snap from the snap.
+ # Note that we can not put it into /usr/bin as '/usr' is different
+ # when the snap uses confinement.
+ cp /usr/bin/snap $IMAGE_HOME
+ export UBUNTU_IMAGE_SNAP_CMD=$IMAGE_HOME/snap
+ /snap/bin/ubuntu-image -w $IMAGE_HOME $IMAGE_HOME/pc.model --channel edge --extra-snaps $IMAGE_HOME/core_*.snap --output $IMAGE_HOME/$IMAGE
+
+ # mount fresh image and add all our SPREAD_PROJECT data
+ kpartx -avs $IMAGE_HOME/$IMAGE
+ # FIXME: hardcoded mapper location, parse from kpartx
+ mount /dev/mapper/loop2p3 /mnt
+ mkdir -p /mnt/user-data/
+ cp -ar /home/gopath /mnt/user-data/
+
+ # create test user home dir
+ mkdir -p /mnt/user-data/test
+ # using symbolic names requires test:test have the same ids
+ # inside and outside which is a pain (see 12345 above), but
+ # using the ids directly is the wrong kind of fragile
+ chown --verbose test:test /mnt/user-data/test
+
+ # we do what sync-dirs is normally doing on boot, but because
+ # we have subdirs/files in /etc/systemd/system (created below)
+ # the writeable-path sync-boot won't work
+ mkdir -p /mnt/system-data/etc/systemd
+ (cd /tmp ; unsquashfs -v $IMAGE_HOME/core_*.snap etc/systemd/system)
+ cp -avr /tmp/squashfs-root/etc/systemd/system /mnt/system-data/etc/systemd/
+
+ # FIXUP silly systemd
+ mkdir -p /mnt/system-data/etc/systemd/system/snapd.service.d
+ cat <<EOF > /mnt/system-data/etc/systemd/system/snapd.service.d/local.conf
+[Unit]
+StartLimitInterval=0
+[Service]
+Environment=SNAPD_DEBUG_HTTP=7 SNAPPY_TESTING=1
+ExecPreStart=/bin/touch /dev/iio:device0
+EOF
+ mkdir -p /mnt/system-data/etc/systemd/system/snapd.socket.d
+ cat <<EOF > /mnt/system-data/etc/systemd/system/snapd.socket.d/local.conf
+[Unit]
+StartLimitInterval=0
+EOF
+
+ umount /mnt
+ kpartx -d $IMAGE_HOME/$IMAGE
+
+ # the reflash magic
+ # FIXME: ideally in initrd, but this is good enough for now
+ cat > $IMAGE_HOME/reflash.sh << EOF
+#!/bin/sh -ex
+mount -t tmpfs none /tmp
+cp /bin/busybox /tmp
+cp $IMAGE_HOME/$IMAGE /tmp
+sync
+# blow away everything
+/tmp/busybox dd if=/tmp/$IMAGE of=/dev/sda bs=4M
+# and reboot
+/tmp/busybox sync
+/tmp/busybox echo b > /proc/sysrq-trigger
+EOF
+ chmod +x $IMAGE_HOME/reflash.sh
+
+ # extract ROOT from /proc/cmdline
+ ROOT=$(cat /proc/cmdline | sed -e 's/^.*root=//' -e 's/ .*$//')
+ cat >/boot/grub/grub.cfg <<EOF
+set default=0
+set timeout=2
+menuentry 'flash-all-snaps' {
+linux /vmlinuz root=$ROOT ro init=$IMAGE_HOME/reflash.sh console=ttyS0
+initrd /initrd.img
+}
+EOF
+}
+
+prepare_all_snap() {
+ # we are still a "classic" image, prepare the surgery
+ if [ -e /var/lib/dpkg/status ]; then
+ setup_reflash_magic
+ REBOOT
+ fi
+
+ # verify after the first reboot that we are now in the all-snap world
+ if [ $SPREAD_REBOOT = 1 ]; then
+ echo "Ensure we are now in an all-snap world"
+ if [ -e /var/lib/dpkg/status ]; then
+ echo "Rebooting into all-snap system did not work"
+ exit 1
+ fi
+ fi
+
+ echo "Wait for firstboot change to be ready"
+ while ! snap changes | grep "Done"; do
+ snap changes || true
+ snap change 1 || true
+ sleep 1
+ done
+
+ echo "Ensure fundamental snaps are still present"
+ . $TESTSLIB/names.sh
+ for name in $gadget_name $kernel_name $core_name; do
+ if ! snap list | grep $name; then
+ echo "Not all fundamental snaps are available, all-snap image not valid"
+ echo "Currently installed snaps"
+ snap list
+ exit 1
+ fi
+ done
+
+ echo "Kernel has a store revision"
+ snap list|grep ^${kernel_name}|grep -E " [0-9]+\s+canonical"
+
+ # Snapshot the fresh state (including boot/bootenv)
+ if [ ! -f $SPREAD_PATH/snapd-state.tar.gz ]; then
+ # we need to ensure that we also restore the boot environment
+ # fully for tests that break it
+ BOOT=""
+ if ls /boot/uboot/*; then
+ BOOT=/boot/uboot/
+ elif ls /boot/grub/*; then
+ BOOT=/boot/grub/
+ else
+ echo "Cannot determine bootdir in /boot:"
+ ls /boot
+ exit 1
+ fi
+
+ systemctl stop snapd.service snapd.socket
+ tar czf $SPREAD_PATH/snapd-state.tar.gz /var/lib/snapd $BOOT
+ systemctl start snapd.socket
+ fi
+}
--- /dev/null
+#!/bin/bash
+
+set -e -x
+
+reset_classic() {
+ systemctl stop snapd.service snapd.socket
+
+ # purge all state
+ sh -x ${SPREAD_PATH}/debian/snapd.postrm purge
+ # extra purge
+ rm -rvf /var/snap
+ mkdir -p /snap /var/snap /var/lib/snapd
+ if [ "$(find /snap /var/snap -mindepth 1 -print -quit)" ]; then
+ echo "postinst purge failed"
+ ls -lR /snap/ /var/snap/
+ exit 1
+ fi
+
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ systemctl start snap.mount.service
+ fi
+
+ rm -rf /root/.snap/gnupg
+ rm -f /tmp/core* /tmp/ubuntu-core*
+
+ if [ "$1" = "--reuse-core" ]; then
+ $(cd / && tar xzf $SPREAD_PATH/snapd-state.tar.gz)
+ mounts="$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')"
+ services="$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ')"
+ systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/
+ for unit in $mounts $services; do
+ systemctl start $unit
+ done
+ fi
+ systemctl start snapd.socket
+
+ # wait for snapd listening
+ while ! printf "GET / HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket; do sleep 0.5; done
+}
+
+reset_all_snap() {
+ # remove all leftover snaps
+ . "$TESTSLIB/names.sh"
+
+ for snap in /snap/*; do
+ snap="${snap:6}"
+ case "$snap" in
+ "bin" | "$gadget_name" | "$kernel_name" | "$core_name" )
+ ;;
+ *)
+ snap remove "$snap"
+ ;;
+ esac
+ done
+
+ # ensure we have the same state as initially
+ systemctl stop snapd.service snapd.socket
+ rm -rf /var/lib/snapd/*
+ $(cd / && tar xzf $SPREAD_PATH/snapd-state.tar.gz)
+ rm -rf /root/.snap
+ systemctl start snapd.service snapd.socket
+}
+
+if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then
+ reset_all_snap "$@"
+else
+ reset_classic "$@"
+fi
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// snapbuild is a minimal executable wrapper around snap building to use for integration tests that need to build snaps under sudo.
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+func main() {
+ if len(os.Args) != 3 {
+ fmt.Fprintf(os.Stderr, "snapbuild: expected sourceDir and targetDir\n")
+ os.Exit(1)
+ }
+
+ snapPath, err := snaptest.BuildSquashfsSnap(os.Args[1], os.Args[2])
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "snapbuild: %v\n", err)
+ os.Exit(1)
+ }
+ fmt.Fprintf(os.Stdout, "built: %s\n", snapPath)
+}
--- /dev/null
+#!/bin/sh
+
+install_local() {
+ local SNAP_NAME="$1"
+ shift;
+ local SNAP_FILE="$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_1.0_all.snap"
+ local SNAP_DIR=$(dirname "$SNAP_FILE")
+ if [ ! -f "$SNAP_FILE" ]; then
+ snapbuild "$SNAP_DIR" "$SNAP_DIR"
+ fi
+ snap install --dangerous "$@" "$SNAP_FILE"
+}
+
+install_local_devmode() {
+ install_local "$1" --devmode
+}
--- /dev/null
+#!/bin/sh
+echo "ok command 1"
--- /dev/null
+#!/bin/sh
+echo "ok command 2"
--- /dev/null
+name: aliases
+version: 1.0
+apps:
+ cmd1:
+ command: bin/cmd1
+ aliases: [alias1]
+ cmd2:
+ command: bin/cmd2
+ aliases: [alias2]
--- /dev/null
+#!/bin/sh
+
+echo "From basic-desktop snap"
--- /dev/null
+[Desktop Entry]
+Name=Echo
+Comment=It echos stuff
+Exec=basic-desktop.echo
+Icon=${SNAP}/meta/gui/icon.png
+Terminal=true
+Type=Application
+Categories=Utilities;Useless;
+Keywords=echo;echo;
+StartupNotify=false
--- /dev/null
+name: basic-desktop
+version: 1.0
+apps:
+ echo:
+ command: bin/echo
--- /dev/null
+#!/bin/sh
+
+echo "configure hook"
--- /dev/null
+#!/bin/sh
+
+echo "invalid hook-- it shouldn't be possible to call me"
--- /dev/null
+name: basic-hooks
+version: 1.0
--- /dev/null
+name: basic
+version: 1.0
+summary: Basic snap
+description: A basic buildable snap
--- /dev/null
+#!/bin/sh
+
+if [ -z $1 ]; then
+ echo "Usage: $0 <filename>"
+ exit 1
+fi
+
+echo "Writing to SNAP_COMMON"
+echo "hello common" > $SNAP_COMMON/$1
+
+echo "Writing to SNAP_DATA"
+echo "hello data" > $SNAP_DATA/$1
+
+# TODO: As soon as `snap run` is used (which creates this directory), uncomment
+# the following lines:
+# echo "Writing to SNAP_USER_COMMON"
+# echo "hello user common" > $SNAP_USER_COMMON/$1
+
+echo "Writing to SNAP_USER_DATA"
+echo "hello user data" > $SNAP_USER_DATA/$1
--- /dev/null
+name: data-writer
+version: 1.0
+apps:
+ app:
+ command: bin/write-data from-app
+ service:
+ command: bin/write-data from-service
+ daemon: oneshot
+ restart-condition: never
--- /dev/null
+#!/bin/sh
+
+echo "error from within configure hook"
+exit 1
--- /dev/null
+name: failing-config-hooks
+version: 1.0
--- /dev/null
+#!/bin/sh
+
+iptables -t nat -"$1" OUTPUT -p tcp -d 172.26.0.15 --dport 8081 -j DNAT --to-destination 127.0.0.1:8081
--- /dev/null
+name: firewall-control-consumer
+version: 1.0
+summary: Basic firewall-control consumer snap
+description: A basic snap declaring a plug on firewall-control
+
+apps:
+ create:
+ command: bin/consumer A
+ plugs: [firewall-control]
+ delete:
+ command: bin/consumer D
+ plugs: [firewall-control]
--- /dev/null
+#!/bin/sh
+
+cat /sys/class/gpio/gpio100
--- /dev/null
+name: gpio-consumer
+version: 1.0
+summary: Basic gpio consumer snap
+description: A basic snap declaring a plug on a gpio slot
+
+apps:
+ read:
+ command: bin/read
+ plugs: [gpio]
--- /dev/null
+#!/bin/sh
+
+set -e
+
+cat /sys/class/block/loop0/stat
--- /dev/null
+name: hardware-observe-consumer
+version: 1.0
+summary: Basic hardware-observe consumer snap
+description: A basic snap declaring a plug on hardware-observe
+
+apps:
+ consumer:
+ command: bin/consumer
+ plugs: [hardware-observe]
--- /dev/null
+#! /usr/bin/env python3
+
+import sys
+
+def main(fileName):
+ try:
+ with open(fileName) as f:
+ print(f.read(), end='')
+ except PermissionError:
+ print('Access to file not allowed')
+ raise
+
+if __name__ == '__main__':
+ main(sys.argv[1])
--- /dev/null
+#! /usr/bin/env python3
+
+import sys
+
+def main(fileName):
+ try:
+ with open(fileName, "a+") as f:
+ msg = "ok\n"
+ f.write(msg)
+ except PermissionError:
+ print('Access to file not allowed')
+ raise
+
+if __name__ == '__main__':
+ main(sys.argv[1])
--- /dev/null
+name: home-consumer
+version: 1.0
+summary: Basic home consumer snap
+description: A basic snap declaring a plug on home
+
+apps:
+ reader:
+ command: bin/reader
+ plugs: [home]
+ writer:
+ command: bin/writer
+ plugs: [home]
--- /dev/null
+#!/bin/sh
+cat /dev/iio:device0
--- /dev/null
+#!/bin/sh
+echo $1 > /dev/iio:device0
--- /dev/null
+name: iio-consumer
+version: 1.0
+summary: Basic iio consumer snap
+description: A basic snap declaring a plug on a iio slot
+
+apps:
+ read:
+ command: bin/read
+ plugs: [iio]
+ write:
+ command: bin/write
+ plugs: [iio]
--- /dev/null
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+def run(key):
+ prefix = key+'='
+ with open('/etc/default/locale') as input_data:
+ for line in input_data:
+ if line.startswith(prefix):
+ print(line[len(prefix):].strip().strip('"'), end='')
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1]))
--- /dev/null
+#!/usr/bin/env python3
+
+import re
+import sys
+
+def set(key, value):
+ with open("/etc/default/locale", "r+") as f:
+ data = f.read()
+ f.seek(0)
+ f.write(replace(key, value, data))
+ f.truncate()
+
+def replace(key, value, content):
+ if key in content:
+ return re.sub(r"^{}=.*$".format(key), "{}=\"{}\"".format(key, value), content, flags=re.MULTILINE)
+ else:
+ return "{}\n{}=\"{}\"\n".format(content, key, value)
+
+if __name__ == '__main__':
+ if len(sys.argv) != 3:
+ print("Required key and value arguments not given")
+ sys.exit(1)
+ sys.exit(set(sys.argv[1], sys.argv[2]))
--- /dev/null
+name: locale-control-consumer
+version: 1.0
+summary: Basic locale-control consumer snap
+description: A basic snap declaring a plug on locale-control
+
+apps:
+ get:
+ command: bin/get
+ plugs: [locale-control]
+ set:
+ command: bin/set
+ plugs: [locale-control]
--- /dev/null
+#!/bin/sh
+
+command="$1"
+shift
+
+"$command" "$@"
--- /dev/null
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+def run():
+ try:
+ subprocess.check_output("tail -n 10 /var/log/syslog", shell=True)
+ print("ok")
+ except Exception as e:
+ print("error accessing log")
+ raise
+
+if __name__ == '__main__':
+ sys.exit(run())
--- /dev/null
+name: log-observe-consumer
+version: 1.0
+summary: Basic log-observe consumer snap
+description: A basic snap declaring a plug on log-observe
+
+apps:
+ log-observe-consumer:
+ command: bin/consumer
+ plugs: [log-observe]
+ cmd:
+ command: bin/cmd
+ plugs: [log-observe]
--- /dev/null
+#!/bin/sh
+
+echo "dummy"
--- /dev/null
+name: modem-manager-consumer
+version: 1.0
+summary: Basic modem-manager consumer snap
+description: A basic snap declaring a plug on modem-manager
+
+apps:
+ modem-manager-consumer:
+ command: bin/consumer
+ slots: [modem-manager]
--- /dev/null
+#!/usr/bin/env python3
+
+import os
+import sys
+
+def run():
+ filename = '/proc/{}/mounts'.format(os.getpid())
+ with open(filename, 'r') as f:
+ print(f.read())
+
+if __name__ == '__main__':
+ sys.exit(run())
\ No newline at end of file
--- /dev/null
+name: mount-observe-consumer
+version: 1.0
+summary: Basic mount-observe consumer snap
+description: A basic snap declaring a plug on mount-observe
+
+apps:
+ mount-observe-consumer:
+ command: bin/consumer
+ plugs: [mount-observe]
--- /dev/null
+#!/usr/bin/env python3
+
+import sys
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+class testRequestHandler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ self.send_response(200)
+
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ message = b"<!doctype html>ok\n"
+ self.wfile.write(message)
+
+def run():
+ server_address = ('localhost', 8081)
+ httpd = HTTPServer(server_address, testRequestHandler)
+ httpd.serve_forever()
+
+if __name__ == '__main__':
+ sys.exit(run())
--- /dev/null
+name: network-bind-consumer
+version: 1.0
+summary: Basic network-bind consumer snap
+description: A basic snap declaring a plug on network-bind
+
+apps:
+ network-consumer:
+ command: bin/consumer
+ daemon: simple
+ plugs: [network-bind]
--- /dev/null
+#! /usr/bin/env python3
+
+import sys
+from socket import timeout
+import urllib.request
+
+if len(sys.argv) > 1:
+ url = sys.argv[1]
+else:
+ url = 'http://www.ubuntu.com'
+
+try:
+ response = urllib.request.urlopen(url, timeout=3)
+ decoded_response = response.read().decode('utf-8')
+ print(decoded_response, end="")
+except urllib.error.URLError as e:
+ print("Error, reason: ", e.reason)
+ sys.exit(1)
+except timeout:
+ print("request timeout")
+ sys.exit(1)
--- /dev/null
+name: network-consumer
+version: 1.0
+summary: Basic network consumer snap
+description: A basic snap declaring a plug on network
+
+apps:
+ network-consumer:
+ command: bin/consumer
+ plugs: [network]
--- /dev/null
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+def run(addr, iface):
+ subprocess.check_call("arp -s {} aa:aa:aa:aa:aa:aa -i {}".format(addr, iface), shell=True)
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1], sys.argv[2]))
--- /dev/null
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+def run():
+ subprocess.check_call("netstat -lnt", shell=True)
+
+if __name__ == '__main__':
+ sys.exit(run())
--- /dev/null
+name: network-control-consumer
+version: 1.0
+summary: Basic network-control consumer snap
+description: A basic snap declaring a plug on network-control
+
+apps:
+ query:
+ command: bin/query
+ plugs: [network-control]
+
+ add-arp-entry:
+ command: bin/add-arp-entry
+ plugs: [network-control]
--- /dev/null
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+def run():
+ subprocess.check_call("netstat -lnt", shell=True)
+
+if __name__ == '__main__':
+ sys.exit(run())
--- /dev/null
+name: network-observe-consumer
+version: 1.0
+summary: Basic network-observe consumer snap
+description: A basic snap declaring a plug on network-observe
+
+apps:
+ network-observe-consumer:
+ command: bin/consumer
+ plugs: [network-observe]
--- /dev/null
+#!/bin/bash
+
+signal="$1"
+pid="$2"
+
+kill -s "$signal" "$pid"
--- /dev/null
+name: process-control-consumer
+version: 1.0
+summary: Basic process-control consumer snap
+description: A basic snap declaring a plug on process-control
+
+apps:
+ signal:
+ command: bin/signal
+ plugs: [process-control]
--- /dev/null
+#!/bin/sh
+
+echo "Getting configuration value for 'foo'"
+if ! output=$(snapctl get foo); then
+ echo "Expected snapctl get to be able to retrieve value for 'foo'"
+ exit 1
+fi
+
+expected_output="bar"
+if "$output" -ne "$expected_output"; then
+ echo "Expected output to be '$expected_output', but it was '$output'"
+ exit 1
+fi
--- /dev/null
+name: snapctl-hooks
+version: 2.0
--- /dev/null
+#!/bin/sh
+
+test_nonexisting() {
+ echo "Getting a configuration value that shouldn't be there"
+ if [ "$(snapctl get non-existing 2>&1)" != "" ]; then
+ echo "Expected getting a non-existing value to be empty"
+ exit 1
+ fi
+}
+
+test_snapctl_set_foo() {
+ echo "Setting foo"
+ if ! snapctl set foo=bar; then
+ echo "snapctl set unexpectedly failed"
+ exit 1
+ fi
+}
+
+test_snapctl_get_foo() {
+ echo "Getting foo"
+ if ! output="$(snapctl get foo)"; then
+ echo "Expected snapctl get to be able to retrieve value just set"
+ exit 1
+ fi
+
+ expected_output="bar"
+ if [ "$output" != "$expected_output" ]; then
+ echo "Expected output to be '$expected_output', but it was '$output'"
+ exit 1
+ fi
+}
+
+test_exit_one() {
+ echo "Failing as requested."
+ exit 1
+}
+
+command=$(snapctl get command)
+case $command in
+ "")
+ ;;
+ "test-nonexisting")
+ test_nonexisting
+ ;;
+ "test-snapctl-set-foo")
+ test_snapctl_set_foo
+ ;;
+ "test-snapctl-get-foo")
+ test_snapctl_get_foo
+ ;;
+ "test-exit-one")
+ test_exit_one
+ ;;
+ *)
+ echo "Invalid command: '$command'"
+ exit 1
+ ;;
+esac
--- /dev/null
+name: snapctl-hooks
+version: 1.0
--- /dev/null
+#!/usr/bin/env python3
+
+import os
+import sys
+
+def run():
+ with open('/proc/tty/drivers', 'r') as f:
+ print(f.read())
+
+if __name__ == '__main__':
+ sys.exit(run())
--- /dev/null
+name: system-observe-consumer
+version: 1.0
+summary: Basic system-observe consumer snap
+description: A basic snap declaring a plug on system-observe
+
+apps:
+ system-observe-consumer:
+ command: bin/consumer
+ plugs: [system-observe]
--- /dev/null
+#!/bin/sh
+echo "ok wellknown 1"
--- /dev/null
+#!/bin/sh
+echo "ok wellknown 2"
--- /dev/null
+name: test-snapd-auto-aliases
+version: 1.0
+apps:
+ wellknown1:
+ command: bin/wellknown1
+ aliases: [test_snapd_wellknown1]
+ wellknown2:
+ command: bin/wellknown2
+ aliases: [test_snapd_wellknown2]
--- /dev/null
+#!/bin/sh
+
+echo "tmp"
+ls -l /tmp
--- /dev/null
+name: test-snapd-classic-confinement
+version: 1.0
+confinement: classic
+apps:
+ test-snapd-classic-confinement:
+ command: bin/classic-confinement
--- /dev/null
+#!/bin/sh
+
+set -e
+
+cat <<EOF
+Please run:
+sudo snap connect content-plug:shared-content-plug content-slot:shared-content-slot
+if you see an permission denied error
+EOF
+
+cat $SNAP/import/shared-content
--- /dev/null
+name: test-snapd-content-plug
+version: 1.0
+apps:
+ content-plug:
+ command: bin/content-plug
+ plugs: [shared-content-plug]
+plugs:
+ shared-content-plug:
+ interface: content
+ target: import
+ content: mylib
+ default-provider: test-snapd-tools
--- /dev/null
+name: test-snapd-content-slot
+version: 1.0
+slots:
+ shared-content-slot:
+ interface: content
+ content: mylib
+ read:
+ - /
--- /dev/null
+Some shared content
+
+Is here!
--- /dev/null
+#!/usr/bin/env python3
+
+import socket
+import sys
+
+def run(snap):
+ clientsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ clientsocket.connect("/run/snapd.socket")
+
+ body = "{ \"action\": \"install\" } "
+ clientsocket.sendall("""POST http://localhost/v2/snaps/{snap} HTTP/1.1
+Host: localhost
+User-Agent: agent
+Content-Type: application/json
+Accept: */*
+Content-Length: {length}
+
+{body}""".format(snap=snap, length=str(len(body)), body=body).encode("utf-8"))
+ return clientsocket.recv(8192)
+
+if __name__ == '__main__':
+ run(sys.argv[1])
--- /dev/null
+#!/usr/bin/env python3
+
+import socket
+import sys
+
+def run():
+ clientsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ clientsocket.connect("/run/snapd.socket")
+
+ clientsocket.sendall("""GET http://localhost/v2/snaps HTTP/1.1
+Host: localhost
+User-Agent: agent
+Accept: */*
+
+""".encode("utf-8"))
+ return clientsocket.recv(8192)
+
+if __name__ == '__main__':
+ print(run())
--- /dev/null
+name: test-snapd-control-consumer
+version: 1.0
+summary: Basic snapd-control consumer snap
+description: A basic snap declaring a plug on snapd-control
+
+apps:
+ install:
+ command: bin/install
+ plugs: [network, snapd-control]
+ list:
+ command: bin/list
+ plugs: [network, snapd-control]
--- /dev/null
+name: test-snapd-cups-control-consumer
+version: 1.0
+summary: Basic cups-control consumer snap
+description: A basic snap declaring a plug on cups-control
+
+apps:
+ lpr:
+ command: lpr
+ plugs: [cups-control, network]
+parts:
+ lpr:
+ plugin: nil
+ stage-packages: [cups-bsd]
--- /dev/null
+name: test-snapd-devmode
+version: 1.0
+summary: Basic snap with devmode confinement
+description: A basic buildable snap that asks for devmode confinement
+confinement: devmode
+apps:
+ test-snapd-devmode:
+ command: /bin/bash
--- /dev/null
+# -*- Mode: Makefile; indent-tabs-mode:t; tab-width: 4 -*-
+
+all:
+
+install:
+ mkdir -p $(DESTDIR)/bin
+ cp -a /usr/share/doc/python-fuse/examples/example/hello.py $(DESTDIR)/bin/create
+ cp -a /usr/share/doc/python-fuse/examples/example/_find_fuse_parts.py $(DESTDIR)/bin
+ chmod a+x $(DESTDIR)/bin/create
--- /dev/null
+name: test-snapd-fuse-consumer
+version: 1.0
+summary: Basic fuse consumer snap
+description: A basic snap declaring a plug on fuse
+
+apps:
+ create:
+ command: bin/create
+ plugs: [fuse-support]
+
+parts:
+ create:
+ plugin: python2
+ build-packages: [python-fuse]
+ stage-packages: [python-fuse]
+ make:
+ plugin: make
+ source: .
--- /dev/null
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+)
+
+func main() {
+ http.HandleFunc("/", handleMainPage)
+
+ log.Println("Starting webserver on :8081")
+ if err := http.ListenAndServe(":8081", nil); err != nil {
+ log.Fatalf("http.ListendAndServer() failed with %s\n", err)
+ }
+}
+
+func handleMainPage(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+
+ fmt.Fprintf(w, "Hello World\n")
+}
--- /dev/null
+name: test-snapd-go-webserver
+version: 16.04-9
+summary: Minimal Golang webserver for snappy
+description: |
+ Mostly a example to show how to build a binary webserver for snappy.
+confinement: strict
+
+apps:
+ webserver:
+ command: ./bin/test-snapd-go-webserver
+ daemon: simple
+ plugs: [network,network-bind]
+
+parts:
+ test-snapd-go-webserver:
+ plugin: go
+ source: .
+ snap:
+ - bin/test-snapd-go-webserver
--- /dev/null
+name: test-snapd-private
+version: 1.0
+summary: Basic snap to be published as private
+description: A basic buildable snap that is expected to be published in private mode
--- /dev/null
+<html>
+<head>
+<title>XKCD rocks!</title>
+</head>
+
+<body>
+ <img src="" id="xkcd">
+ </src>
+
+<div>
+ All comics from <a href="http://xkcd.com">xkcd</a>. It rocks!
+</div>
+</body>
+
+</html>
+
+<script language="javascript">
+function loadJSON(url, callback) {
+
+ var xobj = new XMLHttpRequest();
+ xobj.overrideMimeType("application/json");
+ xobj.open('GET', url, true);
+ xobj.onreadystatechange = function () {
+ if (xobj.readyState == 4 && xobj.status == "200") {
+ callback(xobj.responseText);
+ }
+ };
+ xobj.send(null);
+ }
+
+var base_url = "/xkcd/info.0.json";
+loadJSON(base_url, function(json_data) {
+ var MAX = JSON.parse(json_data).num;
+ var id = Math.floor(Math.random() * MAX) + 1;
+
+ var url = "/xkcd/" + id + "/info.0.json";
+ loadJSON(url, function(json_data) {
+ var data = JSON.parse(json_data);
+ var img = data.img.split("/").pop();
+ document.getElementById("xkcd").src = "/img/xkcd/comics/" + img;
+ });
+});
+</script>
--- /dev/null
+#!/usr/bin/python3
+
+import os
+import sys
+import urllib.request
+
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+
+
+class XkcdRequestHandler(SimpleHTTPRequestHandler):
+
+ XKCD_URL = "http://xkcd.com/"
+ XKCD_IMG_URL = "http://imgs.xkcd.com/"
+
+ def _mini_proxy(self, url):
+ fp = urllib.request.urlopen(url)
+ body = fp.read()
+ info = fp.info()
+ self.send_response(200, "ok")
+ for k, v in info.items():
+ self.send_header(k, v)
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_GET(self):
+ if self.path.startswith("/xkcd/"):
+ url = self.XKCD_URL + self.path[len("/xkcd/"):]
+ return self._mini_proxy(url)
+ elif self.path.startswith("/img/xkcd/"):
+ url = self.XKCD_IMG_URL + self.path[len("/img/xkcd/"):]
+ return self._mini_proxy(url)
+ else:
+ return super(XkcdRequestHandler, self).do_GET()
+
+
+if __name__ == "__main__":
+ # we start in the snappy base directory, ensure we are in "www"
+ os.chdir(os.path.dirname(__file__) + "/../www")
+
+ if len(sys.argv) > 1:
+ port = int(sys.argv[1])
+ else:
+ port = 80
+
+ httpd = HTTPServer(('', port), XkcdRequestHandler)
+ httpd.serve_forever()
--- /dev/null
+name: test-snapd-python-webserver
+version: 16.04-3
+summary: Python based example webserver
+description: |
+ Show random XKCD comic via a build-in webserver
+ This is meant as a fun example for a snappy package.
+apps:
+ test-snapd-python-webserver:
+ command: bin/test-snapd-python-webserver
+ daemon: simple
+ plugs: [network, network-bind]
+
+parts:
+ test-snapd-python-webserver:
+ plugin: python3
+ copy:
+ plugin: dump
+ source: .
+ organize:
+ server.py: bin/test-snapd-python-webserver
+ index.html: www/index.html
--- /dev/null
+#!/bin/sh
+
+echo "service v1"
--- /dev/null
+name: test-snapd-service-try
+version: 1.0
+apps:
+ service:
+ command: bin/service
--- /dev/null
+#!/bin/sh
+
+echo "service v1"
--- /dev/null
+name: test-snapd-service-try
+version: 1.0
+apps:
+ service:
+ command: bin/service
+ daemon: simple
--- /dev/null
+#!/bin/sh
+
+echo "service v1"
--- /dev/null
+name: test-snapd-service
+version: 1.0
+apps:
+ service:
+ command: bin/good
+ daemon: oneshot
+ restart-condition: never
--- /dev/null
+#!/bin/sh
+
+echo "service v2"
+exit 1
--- /dev/null
+name: test-snapd-service
+version: 2.0
+apps:
+ service:
+ command: bin/bad
+ daemon: oneshot
+ restart-condition: never
--- /dev/null
+#!/bin/sh
+
+set -ex
+
+cd $SNAP
+echo "blocking dir $(pwd)"
+
+# add a marker so that applications that rely on this blocker
+# can detect when its ready
+touch $SNAP_DATA/block-running
+
+echo "waiting, press ctrl-c to stop"
+sleep 999999
+exit 0
--- /dev/null
+#!/bin/sh
+
+cat "$@"
--- /dev/null
+#!/bin/sh
+
+command="$1"
+shift
+
+"$command" "$@"
--- /dev/null
+#!/bin/sh
+
+echo "$@"
--- /dev/null
+#!/bin/sh
+
+/usr/bin/env
--- /dev/null
+#!/bin/sh
+
+exit 1
--- /dev/null
+#!/bin/sh
+
+head "$@"
--- /dev/null
+#!/bin/bash
+
+cat <<EOM
+Launching a shell inside the default app confinement. Navigate to your
+app-specific directories with:
+
+ $ cd \$SNAP
+ $ cd \$SNAP_DATA
+ $ cd \$SNAP_USER_DATA
+
+EOM
+
+/bin/bash --norc -i
--- /dev/null
+#!/bin/sh
+
+exit 0
--- /dev/null
+name: test-snapd-tools
+version: 1.0
+apps:
+ echo:
+ command: bin/echo
+ success:
+ command: bin/success
+ fail:
+ command: bin/fail
+ block:
+ command: bin/block
+ cat:
+ command: bin/cat
+ head:
+ command: bin/head
+ env:
+ command: bin/env
+ sh:
+ command: bin/sh
+ cmd:
+ command: bin/cmd
--- /dev/null
+name: test-snapd-upower-observe-consumer
+version: 1.0
+summary: Basic upower-observe consumer snap
+description: A basic snap declaring a plug on upower-observe
+
+apps:
+ upower:
+ command: upower
+ plugs: [upower-observe]
+
+parts:
+ upower:
+ plugin: nil
+ stage-packages: [upower]
--- /dev/null
+#!/bin/sh
+exec /sbin/hwclock -r -f /dev/rtc
--- /dev/null
+#!/bin/sh
+exec /sbin/hwclock --systohc -f /dev/rtc
--- /dev/null
+name: time-control-consumer
+version: 1.0
+summary: Basic time-control consumer snap
+description: A basic snap declaring a plug on a time-control slot
+
+apps:
+ read:
+ command: bin/read
+ plugs: [time-control]
+ write:
+ command: bin/write
+ plugs: [time-control]
--- /dev/null
+#!/bin/sh
+STORE_CONFIG=/etc/systemd/system/snapd.service.d/store.conf
+
+. $TESTSLIB/systemd.sh
+
+_configure_store_backends(){
+ systemctl stop snapd.service snapd.socket
+ mkdir -p $(dirname $STORE_CONFIG)
+ cat > $STORE_CONFIG <<EOF
+[Service]
+Environment=SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=7 SNAPPY_TESTING=1
+Environment=$*
+EOF
+ systemctl daemon-reload
+ systemctl start snapd.socket
+}
+
+setup_fake_store(){
+ local top_dir=$1
+ shift
+ mkdir -p $top_dir/asserts
+ # debugging
+ systemctl status fakestore || true
+ echo "Given a controlled store service is up"
+
+ https_proxy=${https_proxy:-}
+ http_proxy=${http_proxy:-}
+ systemd_create_and_start_unit fakestore "$(which fakestore) -start -dir $top_dir -addr localhost:11028 -https-proxy=${https_proxy} -http-proxy=${http_proxy} $@" "SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=7 SNAPPY_TESTING=1"
+
+ echo "And snapd is configured to use the controlled store"
+ _configure_store_backends "SNAPPY_FORCE_CPI_URL=http://localhost:11028" "SNAPPY_FORCE_SAS_URL=http://localhost:11028"
+}
+
+setup_staging_store(){
+ . "$TESTSLIB/names.sh"
+ echo "Given the core snap is available before switching to staging"
+
+ if [ -z "${core_name}" ]; then
+ snap install core
+ fi
+
+ echo "And snapd is configured to use the staging store"
+ _configure_store_backends "SNAPPY_USE_STAGING_STORE=1"
+}
+
+teardown_store(){
+ local store_type=$1
+ local top_dir=$2
+ if [ "$store_type" = "fake" ]; then
+ systemd_stop_and_destroy_unit fakestore
+ fi
+
+ systemctl stop snapd.socket
+ rm -rf $STORE_CONFIG $top_dir
+ systemctl daemon-reload
+ systemctl start snapd.socket
+}
+
+setup_store(){
+ local store_type=$1
+ local top_dir=$2
+ if [ "$store_type" = "fake" ]; then
+ setup_fake_store $top_dir -assert-fallback
+ else
+ if [ "$store_type" = "staging" ]; then
+ setup_staging_store
+ fi
+ echo "Given a refreshable snap is installed"
+ snap install test-snapd-tools
+ fi
+}
--- /dev/null
+#!/bin/bash
+
+# Use like systemd_create_and_start_unit(fakestore, "$(which fakestore) -start -dir $top_dir -addr localhost:11028 $@")
+systemd_create_and_start_unit() {
+ printf "[Unit]\nDescription=For testing purposes\n[Service]\nType=simple\nExecStart=%s\n" "$2" > /run/systemd/system/$1.service
+ if [ -n "${3:-}" ]; then
+ echo "Environment=$3" >> /run/systemd/system/$1.service
+ fi
+ systemctl daemon-reload
+ systemctl start $1
+}
+
+# Use like systemd_stop_and_destroy_unit(fakestore)
+systemd_stop_and_destroy_unit() {
+ if systemctl status "$1"; then
+ systemctl stop "$1"
+ fi
+ rm -f /run/systemd/system/$1.service
+ systemctl daemon-reload
+}
--- /dev/null
+summary: Check change abort
+
+environment:
+ SNAP_NAME: test-snapd-tools
+
+execute: |
+ echo "Abort with invalid id"
+ if snap abort 10000000; then
+ echo "abort with invalid id should fail"
+ exit 1
+ fi
+
+ echo "===================================="
+
+ echo "Abort with valid id - error"
+ subdirPath="/snap/$SNAP_NAME/current/foo"
+ mkdir -p $subdirPath
+ . $TESTSLIB/snaps.sh
+ if install_local $SNAP_NAME; then
+ echo "install should fail when the target directory exists"
+ exit 1
+ fi
+ idPattern="\d+(?= +Error.*?Install \"$SNAP_NAME\" snap)"
+ id=$(echo "$(snap changes)" | grep -Pzo "$idPattern")
+ if snap abort $id; then
+ echo "abort with valid failed id should fail"
+ exit 1
+ fi
+ rm -rf $subdirPath
+
+ echo "===================================="
+
+ echo "Abort with valid id - done"
+ install_local $SNAP_NAME
+ idPattern="\d+(?= +Done.*?Install \"$SNAP_NAME\" snap)"
+ id=$(echo "$(snap changes)" | grep -Pzo "$idPattern")
+ if snap abort $id; then
+ echo "abort with valid done id should fail"
+ exit 1
+ fi
--- /dev/null
+type: account
+authority-id: testrootorg
+account-id: BGLTY1rcRKQQMbt9B407lDH38lbCW3wg
+display-name: Alice
+timestamp: 2016-09-23T09:03:41Z
+username: alice
+validation: unproven
+sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+
+AcLBUgQAAQoABgUCV+Tv7QAACZ8QAA1rmxGr9MP5K//OKDcEPWE848xPq0ZW61g8JWR0Xq1aaUd6
+mzdCzt6eJJjiOnlIhO6Orf1lmGliISF6MXEebXoF+2GX2tm+qAvqh3p7j4DTSk0Dqv0P1ficJknf
+r+2CnA/BPEGKsnuvcPgc0JYLc+xah4R8JSrCks1d01j4cqTqso0MDACZW/bOWiGZfgnF/dzxO7iR
+RIQHGPehw1OvnGfF+92BirdLfnTSmexvKLIMco7x37YZbYSgUzuWLgwsHic8xLs2jyOjiyP5IWX+
+2hJZlplLV+RyR+wud7+h2GVvoKizILeehb4eaMu1zabjDJX7ANG0svvB2FCWmJaUuclUitLvm5GC
+HJTuB6J12DokGADIGdTNbQD6FCsgcCaDLMyVgxMvteIiBDWY6uvll/rFNSpdxPhkq535q178fKX1
+7kdRK4+QxsChNwHTKMj4mnLC2sg6SlGoKLXnr5OFanDbm/8yMVCXeH7qaWpE0+aVfAo/tXCTbl+G
+m6DDZQ3IGbZpjrCi2O5ocmbyZfEbfcqJlGYV7cr4PKOa9NtplAjv64VD7P3l5oV4XIVgHOIcB2kc
+HfmyJm2AywmTOsN4qqWcKC4hXFza3w6XfJer5uGdi9SyYn7nhFp/sUCWiBKk/s+nemxrP211bfvI
+2weiS2RdvDKDhHbVbfIjcGJuI/7v
--- /dev/null
+type: account-key
+authority-id: testrootorg
+public-key-sha3-384: s2I2irs5PDzHx8n_-lEjkVUn81dvKujEmUiS0c3vwPbwojxDT_QUZ6ejDavhj_yU
+account-id: BGLTY1rcRKQQMbt9B407lDH38lbCW3wg
+name: default
+since: 2016-09-23T11:03:52+02:00
+timestamp: 2016-09-23T09:03:41Z
+body-length: 717
+sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+
+AcbBTQRWhcGAARAA0zc2RM3X30jS651jIn+94BXlljGnvFPxYboFpyu0pkAjTFPsrbfepQ0ZxrAa
+T/I70Exjz3bYDJDPTak///qhiZA+KulaclYjOpleOCKhEVRCssRDqU54it/tbUU33LYD0ef0n1bj
+FUBbe9qWUd9FFNSd4MDe6vCU++7xtpyNgEF/OCeJx3kTCvgYQAMY7z8id340Z0TvjH4YCMTonZb9
+78ed7fh4HnR0m40cGxR+gIC3R/n2MS7Bu1+N6xxHdXwP2agW1gggRthnbhTnNcd3hYgBgHQ4h+bC
+asUlJUM8rB37gfXq6WgEzncLqt/Oh7YhJmPQGqZAhsyRzSm7RVDQuxULirXhz+7JEGSRzE0f9A0v
+95vYl0UqQIdqENDj0DYJgVEH8gQ9nYCzENSKlxMpiEQFxwEwoSqqJK+znc/qye1TdGR204IPiky6
+IeySvrM7hhZjzPu6ZsQDqQZQbFZW+WGtiFFrRFhnlzIh/BMv6Hz2ybkZLkK/gD/H7XTAnnMOOITA
+6CqX7/ZF79phW8iOdnp7AgO+0n5GAoSoUH55r/GZVhfbED20nd6yiXOs6Efoaq1M9Naue7SHbn71
+4bc2QvUIDOLnALr/SD2s6k9bTzYAJnmNCU0pRBbxtSaKUfhoK5m/ALYAvCoWHS+NJl455xO/Ovtf
+/BAVo+oAJL+ByscAEQEAAQ==
+
+AcLBUgQAAQoABgUCV+Tv+AAAj0kQAF598DsZ3lE+qSWTdiGtktvihFmoMIq+m/4l8l+wf+26cdT2
+FStjSFLCfq3isg67vUAIG0pKL5F3i0l+YKQRrpBnIqDBPPZL/HnEo4XFlA/jSJAA5eLwHXBBZ82+
+0+04eR+9hvy6YVXuhTR2Cyv9+7WHw66SHp4+aIFC1/+J0QhqO3xH22jHkmJDnLj5K6JeoViLh3B0
+olTMBtWCfshvrd4QosU2V5gP0tz3qxOQe3cyLVpKOceNf/vCB7BYqynJF66TjNZ1khFye5LSjxFm
+V+f/gIZ2HqzzbpVNsvw49RQ4TNhdN8IwZ4bDZeLc0icm2GkjBHGsSZCIfM7tPHSO5XZt+F61Z5lu
+i72GS69AbTeuPwtSE2XTJkdFWyF2VlhJv64jv5E+Yr9PhX5HxT1VKwUw1eJ2CFRTEujM4rHMSQJ0
+H4a9VN7jOsUR/eJt4pS6GteZIZp525tsNNPNWisBq94WYrbjEkkrqFy9oBUW9cPO5vcIJ0mXrCQi
+z2fUCf25Az/GAdfmrm2fFBW4doecY5M3Fs0Q8YtCpLtmGoevM4bqOk+P0Ko9sQdlNo7yDrBTewGX
+J2Ie/9/iCdOFLcV0Mo9amvbmSq5qlkTgvBneY3XVfwbLHaVjNHUK0USpk0pYF3i8VY2ltz0w5iKH
+dwJ5d4nyG7WUSh8gnn4Wh2IVnLYF
--- /dev/null
+type: account-key
+authority-id: testrootorg
+public-key-sha3-384: kW5sfrKZI2rIAT70JkttRq2VlNa9t8EHOoWrL2ZBAa7tLWZMy2KBweZEh3_MLcZh
+account-id: ct1P6H12NnpJ1nj2jxNX94lHp6sHClxT
+name: default
+since: 2016-09-23T11:09:18+02:00
+timestamp: 2016-09-23T09:09:04Z
+body-length: 717
+sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+
+AcbBTQRWhcGAARAA6ruGAKWOS3ESwR2UwkcqEWJZ+8JjZM+PDuLVjXv6+Bu+vByfyhuOxlScPZs2
+vexsU+HQOvXjTVXBDSvSMNyMxq1SUSUTlqDOOC+F6ZGXfbmRV9zG1YVXn9L2YK7DRSvQ4PywLP4C
+qRbSMcsnjxIgYmXgd1tY70Y01DACn9anVQW2PqZjh1Ite4FLHq8YTgsI9sUQFfpqTitCEZqKt7+D
+EAru5yufXxWi1d6cRjUJCRHZqRpGU0HRVNDe6va66mrYJ9LT8q16merOVhOaS1btuIbRGHbvBwEL
+d3eDk3Do6HiG8oUZ3jrM0J7dqhMBstSA+9YbpMIQZBA4YF+SYOVaMN2X74yUrAuRmcExPFyh1enF
+HA5cNqhK3Cgga8EnHivXhdFoPOJBQwkSOJY1JP/EvJ+V/4hlQHreZgxwwhWfET2mpN46zpnUT3zI
+53pyvQZZl1Heg84gh41fLMAeURrEo5IUDR4IBva5QiN7h/lyhsNzTKjaQaM8lognVUL80hm9UgFQ
+GGwrvxrhZjj7WsbO10ei2p3Z6SEsDihrIbmIJl49fc4KzRcQB+l3TllheLgAVrUJrOaomWTrrrKN
+Iz2maNYX1tmOwGwhQQZr4uo7ve+qC3MOCRHHkCEJpWoHYDyb1cPOhIngwqgq9TjVAO6wx/iJTEGJ
+XAwsu/h4eprPytcAEQEAAQ==
+
+AcLBUgQAAQoABgUCV+TxPgAANGcQADjczRIpexAoQwUfXyz65CmWflIYJIRBr7jFzC2ZIa1uZtDA
+bSF4j1bCrFkSAXjfwmJYmEdBkQeC3H9LV2yPfwj/vxkyic0b7yptrriLBufYS1KbhrBN/gYETDYw
+tGOTSZGlQqZkCiU/nwJz/evG7XmQiL65g4lhwoboilHNvDo2Rhq/An4Sc0Gvlk+UJx1KM5eTWkIO
+7vxQzD9PbsUR/aCvSGTZ5HD1g4rDkWNpVXkXGrME9icp/IqlDlhU/tvAeMkITZlOesd4ENPzn/8W
+lSuxVuwQ0FB+URl+6m/sMkh3p9GA51q3uvE931bXXSElMw455b2B/Idy+UDZEhYFlunAtBAFxRnE
+mz8VG0PchJ5ozBjVG3hf/2Pe9KFHRPhJDWPrlBwo29oc1X93N1xf3vxVI/lQ1HIWXJAYtBuGj8RV
+eX04kPA3piGt1uD4dGVu1FhSvDvnuelkFeBYG7b5uotlAmhW4THCUEGKmeZ/XDLY7980dWYIRduK
+VCHY4pjLBf7KFJ34+EGKdNh3nxqISEoi/sQHyPI+21rLYSzuFTgsUQB3KwE2GlSeAWMzr6PXWn1e
+yADnvpX9bzJJIErGP5PzxyDHPVfXO8eGIdcl2Nj/dSKujSVyq0tYVxjjOBcwxY/AcgYy+D1mpsH3
+/6cJP2vThDIA304fJF0FDLEuU6aa
+
+type: account
+authority-id: testrootorg
+account-id: ct1P6H12NnpJ1nj2jxNX94lHp6sHClxT
+display-name: Bob
+timestamp: 2016-09-23T09:09:04Z
+username: bob
+validation: unproven
+sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
+
+AcLBUgQAAQoABgUCV+TxMAAAa/sQAIoPmlkgThWTaBsQEODgQQCATq3EcFfKz3goeiAgW80cb3G/
+GYcKMUJMfyWAAJtW5wWPBI0XgSSf43bJ8k2S5DNW0xLvFCbgonUjmQ0glRa4st4Pe/DzSpTwitVW
+G5m6Ls8Tr7Tbz+5tKxTZ+/VZnuqhAInHwp1WocMWDtFCPQD/u+KPkuK6a1rpeIIRXjw7uteHmRVK
+dLba2cVN7m9sfGMtkIi9mUOPqabupei6ulVioz0IYZbRR8tWmo4BQ9Qwk5m0S6iRcNe9hKcddrgZ
+W1jCcH3IHtk1nGsssezKIbqucw7W2wMSmWI3LiwzD2R1GPUNC/yb/UP8eJZuo7U39gx9zHJmvXO9
+4ZiJht72bgqrnypgpUBH/rrJM50MlcUjBd6PO3fAMX3h36UT85rj3VS7KLL41R73lySStiXZ9qX/
+URqmFVeqo26izDjqW/Ab6+yI3212N+Gk2MpiqmD8n5kwpR3kVJKma788KaeoqkbgSZCEp3CIuDry
+btHPpbLwenw3LIGR1LZyNWJ+WKiJ/JKecK6xGMkzMpv4xX5SGPvacNwBHoeosSUqsWXxXM93e7Pk
+3LHg6n9alWFRD+SV63WF4ZmhcT9m6wp1EGs9+1aBMgwsr3yxtw6aQyFzMJXp0Jtm5sgvKtKWJKh3
+9q60Y/xBTVnY3tSZZCUAIkGv3J9f
--- /dev/null
+summary: Check snap ack
+systems: [-ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Ack the test store key in case"
+ snap ack $TESTSLIB/assertions/testrootorg-store.account-key
+
+ ALICE_ID=BGLTY1rcRKQQMbt9B407lDH38lbCW3wg
+
+ echo "Ack when missing prerequisite fails"
+ ! snap ack alice.account-key
+
+ echo "Ack account and account-key for alice"
+ snap ack alice.account
+ snap ack alice.account-key
+
+ echo "We got alice account and account-key in the system db"
+ snap known account username=alice | grep "account-id: ${ALICE_ID}"
+ snap known account-key public-key-sha3-384=s2I2irs5PDzHx8n_-lEjkVUn81dvKujEmUiS0c3vwPbwojxDT_QUZ6ejDavhj_yU | grep "account-id: ${ALICE_ID}"
+
+ BOB_ID=ct1P6H12NnpJ1nj2jxNX94lHp6sHClxT
+
+ echo "Ack bob assertions as a stream"
+ snap ack bob.assertions
+
+ echo "We got bob account and account-key in the system db"
+ snap known account username=bob | grep "account-id: ${BOB_ID}"
+ snap known account-key public-key-sha3-384=kW5sfrKZI2rIAT70JkttRq2VlNa9t8EHOoWrL2ZBAa7tLWZMy2KBweZEh3_MLcZh | grep "account-id: ${BOB_ID}"
--- /dev/null
+summary: Check snap alias and snap unalias
+
+prepare: |
+ . $TESTSLIB/snaps.sh
+ install_local aliases
+
+execute: |
+ echo "Sanity check"
+ aliases.cmd1|MATCH "ok command 1"
+ aliases.cmd2|MATCH "ok command 2"
+
+ echo "Default statuses"
+ snap aliases|MATCH "aliases.cmd1 +alias1 +-"
+ snap aliases|MATCH "aliases.cmd2 +alias2 +-"
+
+ echo "Enable aliases"
+ snap alias aliases alias1 alias2
+
+ echo "Enabled statuses"
+ snap aliases|MATCH "aliases.cmd1 +alias1 +enabled"
+ snap aliases|MATCH "aliases.cmd2 +alias2 +enabled"
+
+ echo "Test the aliases"
+ test -h /snap/bin/alias1
+ test -h /snap/bin/alias2
+ alias1|MATCH "ok command 1"
+ alias2|MATCH "ok command 2"
+
+ echo "Disable an alias explicitly"
+ snap unalias aliases alias2
+
+ echo "One disabled status"
+ snap aliases|MATCH "aliases.cmd1 +alias1 +enabled"
+ snap aliases|MATCH "aliases.cmd2 +alias2 +disabled"
+
+ echo "One still works, one is not there"
+ alias1|MATCH "ok command 1"
+ test ! -e /snap/bin/alias2
+ alias2 2>&1|MATCH "alias2: command not found"
+
+ echo "Re-enabling it works"
+ snap alias aliases alias2
+ test -h /snap/bin/alias2
+ alias2|MATCH "ok command 2"
+
+ echo "Both enabled again"
+ snap aliases|MATCH "aliases.cmd1 +alias1 +enabled"
+ snap aliases|MATCH "aliases.cmd2 +alias2 +enabled"
+
+ echo "Reset the aliases to their automatic states (disabled)"
+ snap alias --reset aliases alias1
+
+ echo "One default again"
+ snap aliases|MATCH "aliases.cmd1 +alias1 +-"
+ snap aliases|MATCH "aliases.cmd2 +alias2 +enabled"
+
+ echo "Alias is gone"
+ test ! -e /snap/bin/alias1
+ alias1 2>&1|MATCH "alias1: command not found"
+
+ echo "Re-enabling them works"
+ snap alias aliases alias1 alias2
+ alias1|MATCH "ok command 1"
+ alias2|MATCH "ok command 2"
+
+ echo "Both enabled again"
+ snap aliases|MATCH "aliases.cmd1 +alias1 +enabled"
+ snap aliases|MATCH "aliases.cmd2 +alias2 +enabled"
+
+ echo "Removing the snap should remove the aliases"
+ snap remove aliases
+ test ! -e /snap/bin/alias1
+ test ! -e /snap/bin/alias2
--- /dev/null
+summary: Check that the authentication errors are properly reported.
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+prepare: |
+ mkdir -p /home/test/.snap
+ echo -n "{\"macaroon\":\"yummy\",\"discharges\":[ \"some \"]}" > /home/test/.snap/auth.json
+
+restore: |
+ rm -rf /home/test/.snap install.output connect.output
+
+execute: |
+ echo "An unauthenticated user cannot install snaps"
+ if su - -c "snap install test-snapd-tools 2>${PWD}/install.output" test; then
+ echo "Expected error installing snap from unauthenticated account"
+ exit 1
+ fi
+ expected="error: access denied (try with sudo)"
+ [ "$(cat install.output)" = "$expected" ]
+
+ echo "An unauthenticated user cannot connect plugs to slots"
+ if su - -c "snap connect foo:bar baz:fromp 2>${PWD}/connect.output" test; then
+ echo "Expected error connecting plugs to slots from unauthenticated account"
+ exit 1
+ fi
+ [ "$(cat connect.output)" = "$expected" ]
--- /dev/null
+summary: Check auto-aliases mechanism
+execute: |
+ echo "Install the snap with auto-aliases"
+ snap install test-snapd-auto-aliases
+
+ echo "Test the auto-aliases"
+ test -h /snap/bin/test_snapd_wellknown1
+ test -h /snap/bin/test_snapd_wellknown2
+ test_snapd_wellknown1|MATCH "ok wellknown 1"
+ test_snapd_wellknown2|MATCH "ok wellknown 2"
+
+ echo "Removing the snap should remove the aliases"
+ snap remove test-snapd-auto-aliases
+ test ! -e /snap/bin/test_snapd_wellknown1
+ test ! -e /snap/bin/test_snapd_wellknown2
--- /dev/null
+summary: Run the test suite for C code
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-arm-64]
+environment:
+ EXTRA_PKGS: autoconf automake autotools-dev indent libapparmor-dev libglib2.0-dev libseccomp-dev libudev-dev pkg-config python3-docutils udev
+prepare: |
+ # Install build dependencies for the test
+ dpkg --get-selections > pkg-list
+ apt-get install --yes $EXTRA_PKGS
+ # Remove any autogarbage from sent by developer
+ rm -rf $SPREAD_PATH/cmd/{autom4te.cache,configure,test-driver,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4}
+ find $SPREAD_PATH/cmd/ \( -name Makefile.in -o -name Makefile \) -exec rm -vf {} \;
+ make -C $SPREAD_PATH/cmd distclean || true
+execute: |
+ # Refresh autotools build system
+ cd $SPREAD_PATH/cmd/
+ touch before
+ touch after
+ find . > before
+ autoreconf --install --force
+ # Do an out-of-tree build in the autogarbage directory
+ mkdir -p $SPREAD_PATH/cmd/autogarbage
+ cd $SPREAD_PATH/cmd/autogarbage
+ $SPREAD_PATH/cmd/configure \
+ --prefix=/usr --libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu
+ # Build and run unit tests
+ make -C snap-confine all
+ make -C snap-confine check
+restore: |
+ # Remove autogarbage leftover from testing
+ find $SPREAD_PATH/cmd/ \( -name Makefile.in -o -name Makefile \) -exec rm -vf {} \;
+ rm -rf $SPREAD_PATH/cmd/{autom4te.cache,configure,test-driver,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4}
+ # Remove the build tree
+ rm -rf $SPREAD_PATH/cmd/autogarbage/
+ # Remove any installed packages
+ dpkg --set-selections < pkg-list
+ rm -f pkg-list
+debug: |
+ # Show the test suite failure log if there's one
+ cat $SPREAD_PATH/cmd/autogarbage/snap-confine/tests/test-suite.log || true
+ # Show any autogabare that may need cleaning
+ cd $SPREAD_PATH/cmd/
+ find . > after
+ diff -u $SPREAD_PATH/cmd/before $SPREAD_PATH/cmd/after
+ # Show kernel log as it may contain useful stuff
+ tail /var/log/kern.log
--- /dev/null
+summary: Checks for cli errors of the change command.
+
+execute: |
+ echo "When an invalid ID is given to the change command it shows an error"
+ if snap change 10000000; then
+ echo "Expected error when trying change on invalid ID" && exit 1
+ fi
--- /dev/null
+summary: test chattr
+# ubuntu-core doesn't have go :-)
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2503
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+prepare: |
+ go build -o toggle ./toggle.go
+execute: |
+ touch foo
+ # no immutable flag:
+ lsattr foo | grep -qv i
+ test "$(./toggle foo)" = "mutable -> immutable"
+ # and now an immutable flag!:
+ lsattr foo | grep -q i
+ test "$(./toggle foo)" = "immutable -> mutable"
+ # no immutable flag again:
+ lsattr foo | grep -qv i
--- /dev/null
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+func die(err error) {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+}
+
+func main() {
+ if len(os.Args) < 2 {
+ die(fmt.Errorf("usage: %s file", os.Args[0]))
+ }
+
+ f, err := os.Open(os.Args[1])
+ if err != nil {
+ die(err)
+ }
+
+ before, err := osutil.GetAttr(f)
+ if err != nil {
+ die(err)
+ }
+
+ err = osutil.SetAttr(f, before^osutil.FS_IMMUTABLE_FL)
+ if err != nil {
+ die(err)
+ }
+
+ after, err := osutil.GetAttr(f)
+ if err != nil {
+ die(err)
+ }
+
+ if before&osutil.FS_IMMUTABLE_FL != 0 {
+ fmt.Print("immutable")
+ } else {
+ fmt.Print("mutable")
+ }
+ fmt.Print(" -> ")
+ if after&osutil.FS_IMMUTABLE_FL != 0 {
+ fmt.Println("immutable")
+ } else {
+ fmt.Println("mutable")
+ }
+}
--- /dev/null
+summary: Ensure that classic confinement works
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+prepare: |
+ . $TESTSLIB/snaps.sh
+ snapbuild "$TESTSLIB/snaps/test-snapd-classic-confinement/" .
+
+execute: |
+ echo "Check that classic snaps work only with --classic"
+ if snap install --dangerous test-snapd-classic-confinement_1.0_all.snap; then
+ echo "snap install needs --classic to install snaps with classic confinment"
+ exit 1
+ fi
+
+ echo "Check that the classic snap works (it skips the entire sandbox)"
+ snap install --dangerous --classic test-snapd-classic-confinement_1.0_all.snap
+ touch /tmp/lala
+ test-snapd-classic-confinement | MATCH lala
+ snap remove test-snapd-classic-confinement
+
+ echo "Check that we can install classic confinement snaps from the store"
+ snap install --classic test-snapd-classic-confinement
+ snap list | MATCH "test-snapd-classic-confinement .*1.0 .*classic"
+ snap info test-snapd-classic-confinement|MATCH "installed:.* 1.0 .*classic"
+ test-snapd-classic-confinement | MATCH lala
+
+ echo "Snap refresh from the store also works (2.0 is in beta, 1.0 in stable)"
+ snap refresh --beta test-snapd-classic-confinement
+ snap list | MATCH "test-snapd-classic-confinement .*2.0 .*classic"
+ snap info test-snapd-classic-confinement|MATCH "installed:.* 2.0 .*classic"
+ test-snapd-classic-confinement | MATCH lala
+
--- /dev/null
+summary: Check that cmdline for channel shortcuts work
+execute: |
+ echo Conflicting channel commandline errors correctly
+ if snap install --beta --edge test-snapd-tools 2>err.msg; then
+ echo "Expected failure when --beta --edge is given at the same time"
+ exit 1
+ fi
+ cat err.msg
+ grep "Please specify a single channel" err.msg
--- /dev/null
+source lib.exp0
+
+# abort completes with change ids
+rechat "snap abort \t\t" "1 *2"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# ack completes with directories and files
+chat "snap ack ./\t\t" "ack.exp" true
+chat "" " testdir/"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# buy completes remote snaps only
+chat "snap buy \t\t" "snap buy ??$"
+chat "test-\t\t" "test-assumes*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# change completes with change ids
+rechat "snap change \t\t" "1 *2"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap delete-key \t" "snap delete-key default"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap disable \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# download with 3+ chars completes store stuff
+chat "snap download test-\t\t" "test-assumes"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap enable \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap export-key \t" "snap export-key default"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap get \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# info completes directories and snap files, and locally installed snaps
+chat "snap inf\t\t\t" "bar.snap*core*testdir/"
+
+# with 3+ chars, also remote snaps
+chat "test-\t\t" "test-assumes"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# install completes directories and snaps
+chat "snap ins\t\t\t" "bar.snap*testdir/"
+
+# install with 3+ chars also completes store stuff
+chat "tes\t\t\t" "test-assumes" true
+# TODO: don't list things already installed:
+chat "" "test-snapd-tools" true
+chat "" "testdir/"
+cancel
+
+# no extra space added after a directory
+chat "snap ins\t./tes\t" " ./testdir/$"
+cancel
+
+# extra space added after a file
+chat "snap ins\t./bar\t" " ./bar.snap $"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# create a key doesn't complete
+chat "snap create-key \t\t" "snap create-key ??$"
+chat "\r" "Passphrase:"
+chat "pass\r" "Confirm passphrase:"
+send "pass\r"
+
+# this can take a while
+set timeout 60
+
+next
+cancel
+brexit
+
--- /dev/null
+set send_slow {1 0.1}
+set timeout 5
+
+proc delay {} {
+ sleep 0.5
+}
+
+proc chat {outstr instr {keep false}} {
+ send $outstr
+ delay
+ if $keep {
+ expect {
+ timeout {puts "timeout"; exit 1}
+ -notransfer $instr
+ }
+ } else {
+ expect {
+ timeout {puts "timeout"; exit 1}
+ $instr
+ }
+ }
+}
+
+proc rechat {outstr inre {keep false}} {
+ send $outstr
+ delay
+ if $keep {
+ expect {
+ timeout {puts "timeout"; exit 1}
+ -notransfer -re $inre
+ }
+ } else {
+ expect {
+ timeout {puts "timeout"; exit 1}
+ -re $inre
+ }
+ }
+}
+
+proc next {} {
+ delay
+ expect {
+ timeout {puts "timeout"; exit 1}
+ "bash-*\[$#] $"
+ }
+}
+
+proc cancel {} {
+ send "\v\15\r"
+ next
+}
+
+proc brexit {} {
+ send "exit\r"
+ expect {
+ timeout {puts "timeout"; exit 1}
+ eof {exit 0}
+ }
+}
+
+
+# set up
+spawn bash --norc -i
+next
+send ". /etc/bash_completion; . ../../../data/completion/snap\n"
+next
--- /dev/null
+source lib.exp0
+
+# list completes locally installed snaps
+chat "snap list \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap refresh \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap remove \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap revert \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap set \t\t" "core*test-snapd-tools"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap sign-build -k\t" "kdefault"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap sign -k\t" "kdefault"
+
+cancel
+brexit
--- /dev/null
+summary: Check different completions
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+prepare: |
+ mkdir -p testdir
+ touch testdir/foo.snap
+ touch bar.snap
+ snap install core
+ snap install test-snapd-tools
+ . "$TESTSLIB/mkpinentry.sh"
+ expect -d -f key.exp0
+
+restore: |
+ rm testdir/foo.snap bar.snap
+ rmdir testdir
+
+execute: |
+ for i in *.exp; do
+ echo $i
+ expect -d -f $i
+ done
--- /dev/null
+source lib.exp0
+
+# --help completes
+chat "snap --h\t" "snap --help $" true
+# but no more
+chat "\t\t" "snap --help $"
+cancel
+
+# --version completes
+chat "snap --v\t" "snap --version $" true
+# but no more
+chat "\t\t" "snap --version $"
+cancel
+
+# commands complete
+rechat "snap in\t\t" "\[\n ]info\[\r ]" true
+rechat "" "\[\n ]install\[\r ]" true
+rechat "" "\[\n ]interfaces\[\r ]"
+
+chat "s\t" "snap install $"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+chat "snap try \t" "testdir/"
+
+cancel
+brexit
--- /dev/null
+source lib.exp0
+
+# watch completes with change ids
+rechat "snap watch \t\t" "1 *2"
+
+cancel
+brexit
--- /dev/null
+summary: trivial snap with classic confinement runs correctly
+details: |
+ This test checks that a very much trivial "hello-world"-like snap using
+ classic confinement can be executed correctly. There are two variants of
+ this test (classic and jailmode) and the snap (this particular one) should
+ function correctly in both cases.
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-64-fixme]
+environment:
+ INSTALL_FLAGS/classic: --classic
+ INSTALL_FLAGS/classic_and_jailmode: --classic --jailmode
+prepare: |
+ make -C test-snapd-hello-classic clean
+ make -C test-snapd-hello-classic
+ snap install $INSTALL_FLAGS --dangerous ./test-snapd-hello-classic/test-snapd-hello-classic_0.1_*.snap
+execute: |
+ /snap/bin/test-snapd-hello-classic | MATCH 'Hello Classic!'
+restore: |
+ make -C test-snapd-hello-classic clean
--- /dev/null
+SNAP_NAME = test-snapd-hello-classic
+SNAP_VERSION = 0.1
+
+# Ask gcc about the architecture name
+arch := $(shell $(CC) -dumpmachine)
+
+ifeq ($(arch),x86_64-linux-gnu)
+snap_arch = amd64
+dynamic_linker=ld-linux-x86-64.so.2
+else ifeq ($(arch),i686-linux-gnu)
+# NOTE: arch needs to be i386-linux-gnu, not i686-linux-gnu
+arch := i386-linux-gnu
+snap_arch = i386
+dynamic_linker=ld-linux.so.2
+else ifeq ($(arch),aarch64-linux-gnu)
+snap_arch = arm64
+dynamic_linker=ld-linux-aarch64.so.1
+else ifeq ($(arch),arm-linux-gnueabihf)
+snap_arch = armhf
+dynamic_linker=ld-linux-armhf.so.3
+else ifeq ($(arch),powerpc64le-linux-gnu)
+# NOTE: The architecture name is ppc64el (E-L) but GCC uses powerpc64le (L-E)
+snap_arch = ppc64el
+dynamic_linker=ld64.so.2
+else
+$(error cannot translate architecture $(arch) to snap equivalent)
+endif
+
+# Name of the snap we're building
+snap_file = $(SNAP_NAME)_$(SNAP_VERSION)_$(snap_arch).snap
+
+define snap_yaml
+name: $(SNAP_NAME)
+version: $(SNAP_VERSION)
+summary: A hello-world with classic confinement
+architectures: [$(snap_arch)]
+apps:
+ $(SNAP_NAME):
+ command: test-snapd-hello-classic.$(snap_arch).bin
+confinement: classic
+endef
+
+.PHONY: all
+all: $(snap_file)
+
+# Name of the core snap to use
+snap_core=core
+
+# Don't search in default locations
+LDFLAGS += -Wl,-z,nodefaultlib
+LDFLAGS += -Wl,--enable-new-dtags
+# Search in the core snap
+LDFLAGS += -Wl,-rpath,/snap/$(snap_core)/current/lib/$(arch):/snap/$(snap_core)/current/usr/lib/$(arch)
+# Use the dynamic linker from the core snap
+LDFLAGS += -Wl,--dynamic-linker=/snap/$(snap_core)/current/lib/$(arch)/$(dynamic_linker)
+
+test-snapd-hello-classic.$(snap_arch).bin: test-snapd-hello-classic.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<
+
+$(snap_file): test-snapd-hello-classic.$(snap_arch).bin meta/snap.yaml
+ mksquashfs . $@ -e $@ -noappend -no-xattrs -comp xz
+
+meta: Makefile
+ mkdir -p $@
+
+export snap_yaml
+meta/snap.yaml: Makefile | meta
+ echo "$$snap_yaml" > $@
+
+.ONESHELL:
+
+.PHONY: clean
+clean:
+ rm -f test-snapd-hello-classic.*.bin
+ rm -f meta/snap.yaml
+ rm -f *.snap
+ rm -f -d meta
--- /dev/null
+#include <stdio.h>
+
+int main()
+{
+ printf("Hello Classic!\n");
+ return 0;
+}
--- /dev/null
+spawn snap create-key
+
+expect "Passphrase: "
+send "one\n"
+
+expect "Confirm passphrase: "
+send "two\n"
+
+expect {
+ "error: passphrases do not match" {
+ exit 0
+ } default {
+ exit 1
+ }
+}
--- /dev/null
+set timeout 60
+
+spawn snap create-key
+
+expect "Passphrase: "
+send "pass\n"
+
+expect "Confirm passphrase: "
+send "pass\n"
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
+
+set timeout 60
+
+spawn snap keys
+
+expect {
+ "default " {}
+ timeout { exit 1 }
+ eof { exit 1 }
+}
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
+
+spawn snap export-key --account=developer default
+
+# fun!
+# gpg1 asks for a passphrase on the terminal no matter what
+# gpg2 gets the passphrase via our fake pinentry
+expect {
+ "Enter passphrase: " {send "pass\n"; exp_continue}
+ "account-id: developer" {}
+ timeout { exit 1 }
+ eof { exit 1 }
+}
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
--- /dev/null
+set timeout 60
+
+spawn snap create-key another
+
+expect "Passphrase: "
+send "pass\n"
+
+expect "Confirm passphrase: "
+send "pass\n"
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
+
+set timeout 60
+
+spawn snap keys
+
+expect {
+ "another " {}
+ timeout { exit 1 }
+ eof { exit 1 }
+}
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
+
+spawn snap export-key --account=developer another
+
+# fun!
+# gpg1 asks for a passphrase on the terminal no matter what
+# gpg2 gets the passphrase via our fake pinentry
+expect {
+ "Enter passphrase: " {send "pass\n"; exp_continue}
+ "account-id: developer" {}
+ timeout { exit 1 }
+ eof { exit 1 }
+}
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
--- /dev/null
+summary: Checks for snap create-key
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+prepare: |
+ . "$TESTSLIB/mkpinentry.sh"
+
+execute: |
+ echo "Checking passphrase mismatch error"
+ expect -d -f passphrase_mismatch.exp
+
+ echo "Checking successful default key pair generation"
+ expect -d -f successful_default.exp
+
+ echo "Checking successful non-default key pair generation"
+ expect -d -f successful_non_default.exp
--- /dev/null
+summary: Ensure create-user functionality
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+environment:
+ USER_EMAIL: mvo@ubuntu.com
+ USER_NAME: mvo
+
+restore: |
+ userdel -r $USER_NAME || true
+ rm -rf /etc/sudoers.d/create-user-$USER_NAME
+
+execute: |
+ echo "snap create-user -- ensure failure when run as non-root user without sudo"
+ expected="error: while creating user: access denied"
+ if obtained=$(su - test /bin/sh -c "snap create-user $USER_EMAIL 2>&1"); then
+ echo "create-user command should have failed"
+ fi
+ [[ "$obtained" =~ "$expected" ]]
+
+ echo "snap create-user -- ensure success when run as non-root user with sudo"
+ expected="created user \"$USER_NAME\""
+ obtained=$(su - test /bin/sh -c "sudo snap create-user --force-managed --sudoer $USER_EMAIL 2>&1")
+ [[ "$obtained" =~ "$expected" ]]
+
+ echo "ensure user exists in /etc/passwd"
+ grep -qE "^$USER_NAME:x:[0-9]+:[0-9]+:$USER_EMAIL" /etc/passwd
+
+ echo "ensure proper sudoers.d file"
+ grep -q "$USER_NAME ALL=(ALL) NOPASSWD:ALL" /etc/sudoers.d/create-user-$USER_NAME
--- /dev/null
+summary: Check that systemd units are enabled/disabled and gpio works after rebooting
+
+details: |
+ This test makes sure that the systemd snippet created by the gpio interface
+ is executed after a reboot.
+
+ It modifies the core snap to provide a gpio slot. Also, a mocked gpio node and the
+ required systemfs files (export and unexeport) are created as a bind mount. The test
+ expects that, after a snap declared a gpio plug is installed and connected, after
+ a reboot the systemd service tries to regenerate the gpio device node if it does not
+ find it.
+
+systems: [ubuntu-core-16-64]
+
+environment:
+ GPIO_MOCK_DIR: /home/test/gpio-mock
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Given a snap declaring a plug on gpio is installed"
+ . $TESTSLIB/snaps.sh
+ install_local gpio-consumer
+
+ echo "And a mocked gpio device is in place"
+ cat > /home/test/gpio-mock.sh <<-EOF
+ #!/bin/sh
+ if [ ! -d "$GPIO_MOCK_DIR" ]; then
+ # the service has just been created
+ mkdir -p $GPIO_MOCK_DIR
+ touch $GPIO_MOCK_DIR/gpio100 $GPIO_MOCK_DIR/export $GPIO_MOCK_DIR/unexport
+ else
+ # after reboot, remove device node to test export
+ rm $GPIO_MOCK_DIR/gpio100
+ truncate -s 0 $GPIO_MOCK_DIR/export
+ fi
+ mount --bind $GPIO_MOCK_DIR /sys/class/gpio
+ EOF
+ chmod a+x /home/test/gpio-mock.sh
+
+ cat > /etc/systemd/system/gpio-mock.service <<-EOF
+ [Unit]
+ Description=Set up mock for gpio test
+ Before=snap.core.interface.gpio-100.service
+
+ [Service]
+ Type=oneshot
+ RemainAfterExit=true
+ ExecStart=/home/test/gpio-mock.sh
+ ExecStop=
+
+ [Install]
+ WantedBy=multi-user.target
+ EOF
+ systemctl enable --now gpio-mock.service
+
+ echo "And the gpio plug is connected"
+ . "$TESTSLIB/names.sh"
+ snap connect gpio-consumer:gpio ${core_name}:gpio-pin
+
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ umount /sys/class/gpio || true
+ systemctl disable gpio-mock.service
+ rm -rf $GPIO_MOCK_DIR /etc/systemd/system/gpio-mock.service /home/test/gpio-mock.sh
+
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ echo "Then the snap service units concerning the gpio device must be run before and after a reboot"
+ expected="Unit snap.core.interface.gpio-100.service has finished starting up"
+ journalctl -xe --no-pager | grep "$expected"
+
+ if [ "$SPREAD_REBOOT" = "1" ]; then
+ cat $GPIO_MOCK_DIR/export | grep -P "^100$"
+ fi
+
+ if [ "$SPREAD_REBOOT" = "0" ]; then
+ REBOOT
+ fi
--- /dev/null
+summary: Check that enable/disable works
+
+execute: |
+ echo "Install test-snapd-tools and ensure it runs"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ test-snapd-tools.echo Hello|grep Hello
+ echo "Disable test-snapd-tools and ensure it is listed as disabled"
+ snap disable test-snapd-tools|grep disabled
+
+ echo "Ensure the test-snapd-tools command is no longer there"
+ if ls /snap/bin/test-snapd-tools*; then
+ echo "test-snapd-tools binaries are not disabled"
+ exit 1
+ fi
+
+ echo "Enable test-snapd-tools again and ensure it is no longer listed as disabled"
+ snap enable test-snapd-tools|grep -v disabled
+ echo "Ensure test-snapd-tools runs normally after it was enabled"
+ test-snapd-tools.echo Hello |grep Hello
+
+ echo "Ensure the important snaps can not be disabled"
+ . $TESTSLIB/names.sh
+ for sn in $core_name $kernel_name $gadget_name; do
+ if snap disable $sn; then
+ echo "It should not be possible to disable $sn"
+ exit 1
+ fi
+ done
--- /dev/null
+spawn snap login $env(SPREAD_STORE_USER)
+
+expect "Password: "
+send "$env(SPREAD_STORE_PASSWORD)\n"
+
+expect {
+ "Login successful" {
+ exit 0
+ } default {
+ exit 1
+ }
+}
--- /dev/null
+summary: Check that find works with private snaps.
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+details: |
+ These tests rely on the existence of a snap in the production store set to private.
+
+ In order to do the full checks, it also needs the credentials of the owner of that
+ snap set in the environment variables SPREAD_STORE_USER and SPREAD_STORE_PASSWORD, if
+ they are not present then only the negative check (private snap does not show up in
+ the find results without specifying private search or without the owner logged) is
+ performed.
+
+restore: |
+ snap logout || true
+
+execute: |
+ echo "When a snap is private it doesn't show up in the find without login and without specifying private search"
+ ! snap find test-snapd-private | grep -q "test-snapd-private +\d+\.\d+"
+
+ echo "When a snap is private it doesn't show up in the find --private results without login"
+ ! snap find test-snapd-private --private | grep -q "test-snapd-private +\d+\.\d+"
+
+ echo "Given account store credentials are available"
+ if [ ! -z "$SPREAD_STORE_USER" ] && [ ! -z "$SPREAD_STORE_PASSWORD" ]; then
+ echo "And the user has logged in"
+ expect -f successful_login.exp
+
+ echo "Then a private snap belonging to that user shows up in the find results"
+ snap find test-snapd-private --private | grep -q "test-snapd-private +\d+\.\d+"
+ fi
--- /dev/null
+summary: Check that firstboot assertions are imported
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+environment:
+ SEED_DIR: /var/lib/snapd/seed
+prepare: |
+ systemctl stop snapd.service
+ rm -f /var/lib/snapd/state.json
+ mkdir -p $SEED_DIR/assertions
+ touch $SEED_DIR/seed.yaml
+ # pretend to be not classic :)
+ mv /etc/os-release /etc/os-release.save
+ cp $TESTSLIB/os-release.16 /etc/os-release
+ echo Copy the needed assertions to /var/lib/snapd/
+ cp $TESTSLIB/assertions/developer1.account $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1.account-key $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1-pc.model $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/testrootorg-store.account-key $SEED_DIR/assertions
+restore: |
+ mv /etc/os-release.save /etc/os-release
+ systemctl start snapd.service
+execute: |
+ echo "Start the daemon with an empty state, this will make it import "
+ echo "assertions from the $SEED_DIR/assertions subdirectory."
+ systemctl start snapd.service
+
+ echo "Wait for Seed change to be finished"
+ while ! snap known model|grep "type: model"; do
+ sleep 1
+ done
+
+ echo "Verifying the imported assertions"
+ if [ $(snap known model|grep "type: model"|wc -l) != "1" ]; then
+ echo "Model assertion was not imported on firstboot"
+ exit 1
+ fi
--- /dev/null
+summary: Check that firstboot snaps are installed
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+environment:
+ SEED_DIR: /var/lib/snapd/seed
+prepare: |
+ snapbuild $TESTSLIB/snaps/basic .
+
+ systemctl stop snapd.service snapd.socket
+ rm -f /var/lib/snapd/state.json
+ mkdir -p $SEED_DIR/snaps
+ mkdir -p $SEED_DIR/assertions
+ cat > $SEED_DIR/seed.yaml <<EOF
+ snaps:
+ - name: basic
+ unasserted: true
+ file: basic.snap
+ EOF
+ # pretend to be not classic :)
+ mv /etc/os-release /etc/os-release.save
+ cp $TESTSLIB/os-release.16 /etc/os-release
+
+ echo Copy the needed snaps to $SEED_DIR/snaps
+ cp ./basic_1.0_all.snap $SEED_DIR/snaps/basic.snap
+restore: |
+ rm -r $SEED_DIR
+ mv /etc/os-release.save /etc/os-release
+ systemctl start snapd.socket snapd.service
+execute: |
+ echo "Start the daemon with an empty state, this will make it import "
+ echo "assertions from the $SEED_DIR/assertions subdirectory."
+ systemctl start snapd.socket snapd.service
+
+ echo "Wait for Seed change to be finished"
+ for i in `seq 60`; do
+ if snap list 2>/dev/null | grep -q ^basic; then
+ break
+ fi
+ done
+
+ snap list | grep ^basic
+ test -f $SEED_DIR/snaps/basic.snap
--- /dev/null
+summary: Check that snapd builds with gccgo
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-14.04-64]
+prepare: |
+ echo Installing gccgo-6 and pretending it is the default go
+ apt install -y gccgo-6
+ ln -s /usr/bin/go-6 /usr/local/bin/go
+restore: |
+ rm -f /usr/local/bin/go
+ apt-get autoremove -y gccgo-6
+execute: |
+ echo Ensure we really build with gccgo
+ go version|MATCH gccgo
+ echo Build the deb with gccgo and run the tests as part of the build
+ su - -c "cd $GOPATH/src/github.com/snapcore/snapd && dpkg-buildpackage -tc -Zgzip" test
+
+# Tests run during package build take a while.
+warn-timeout: 8m
+kill-timeout: 20m
--- /dev/null
+summary: Check commands help
+environment:
+ CMD/abort: abort
+ CMD/changes: changes
+ CMD/find: find
+ CMD/install: install
+ CMD/interfaces: interfaces
+ CMD/remove: remove
+execute: |
+ echo "Checking help for command $CMD"
+ expected="(?s)Usage:\n snap \[OPTIONS\] $CMD.*?\n\nThe $CMD command .*?\nHelp Options:\n -h, --help +Show this help message\n.*?"
+ snap $CMD --help | grep -Pzq "$expected"
--- /dev/null
+summary: Test that i18n works
+
+execute: |
+ LANG=de_DE.UTF-8 snap changes everything | MATCH "Ja, ja, allerdings."
--- /dev/null
+summary: Checks for cli errors installing snaps
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+environment:
+ SNAP_NAME: test-snapd-tools
+ # Ensure that running purely from the deb (without re-exec) works
+ # correctly
+ SNAP_REEXEC/noreexec: 0
+ SNAP_REEXEC/withreexec: 1
+
+prepare: |
+ echo "Given a snap with a failing command is installed"
+ . $TESTSLIB/snaps.sh
+ install_local $SNAP_NAME
+
+execute: |
+ echo "Install unexisting snap prints error"
+ if snap install unexisting.canonical; then
+ echo "Installing unexisting snap should fail"
+ exit 1
+ fi
+
+ echo "============================================"
+
+ echo "Install without snap name shows error"
+ if snap install; then
+ echo "Installing without snap name should fail"
+ exit 1
+ fi
+
+ echo "============================================"
+
+ echo "Install points to sudo when not authenticated"
+ if su - -c "snap install $SNAP_NAME 2>${PWD}/install.output" test; then
+ echo "Unauthenticated install should fail"
+ exit 1
+ fi
+ grep "try with sudo" install.output
+
+ echo "============================================"
+
+ echo "Calling a failing command from a snap should fail"
+ if test-snapd-tools.fail; then
+ echo "Failing snap commands should keep failing after installed"
+ exit 1
+ fi
+
+ echo "============================================"
+
+ echo "Install a snap that is already installed shows a message"
+ echo "but does "exit 0" (LP: #1622782)"
+ snap install $SNAP_NAME 2> stderr.out
+ cat stderr.out | MATCH "snap \"$SNAP_NAME\" is already installed"
--- /dev/null
+summary: Check that install/remove of multiple snaps works
+
+execute: |
+ echo "Install multiple snaps from the store"
+ snap install test-snapd-tools test-snapd-control-consumer
+ snap list | MATCH test-snapd-tools
+ snap list | MATCH test-snapd-control-consumer
+
+ echo "Remove of multiple snaps works"
+ snap remove test-snapd-tools test-snapd-control-consumer
+ snap list | MATCH -v test-snapd-tools
+ snap list | MATCH -v test-snapd-control-consumer
--- /dev/null
+summary: Checks for snap sideload install
+
+prepare: |
+ for snap in basic test-snapd-tools basic-desktop test-snapd-devmode
+ do
+ snapbuild $TESTSLIB/snaps/$snap .
+ done
+
+environment:
+ # Ensure that running purely from the deb (without re-exec) works
+ # correctly
+ SNAP_REEXEC/reexec0: 0
+ SNAP_REEXEC/reexec1: 1
+
+restore: |
+ for snap in basic test-snapd-tools basic-desktop
+ do
+ rm ./${snap}_1.0_all.snap
+ done
+
+execute: |
+ echo "Sideloaded snap shows status"
+ expected="(?s)basic 1.0 installed\n\
+ .*"
+ snap install --dangerous ./basic_1.0_all.snap | grep -Pzq "$expected"
+
+ echo "Sideloaded snap with (deprecated) --force-dangerous option"
+ snap remove basic
+ snap install --force-dangerous ./basic_1.0_all.snap | grep -Pzq "$expected"
+
+ echo "Sideloaded snap executes commands"
+ snap install --dangerous ./test-snapd-tools_1.0_all.snap
+ test-snapd-tools.success
+ [ "$(test-snapd-tools.echo Hello World)" = "Hello World" ]
+
+ echo "Sideload desktop snap"
+ snap install --dangerous ./basic-desktop_1.0_all.snap
+ expected="\[Desktop Entry\]\n\
+ Name=Echo\n\
+ Comment=It echos stuff\n\
+ Exec=env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/basic-desktop_echo.desktop /snap/bin/basic-desktop.echo\n"
+ cat /var/lib/snapd/desktop/applications/basic-desktop_echo.desktop | grep -Pzq "$expected"
+
+ echo "Sideload devmode snap fails without flags"
+ expected="requires devmode or confinement override"
+ ( snap install --dangerous ./test-snapd-devmode_1.0_all.snap 2>&1 || true ) | grep -Pzq "$expected"
+
+ echo "Sideload devmode snap succeeds with --devmode"
+ expected="test-snapd-devmode 1.0 installed"
+ snap install --devmode ./test-snapd-devmode_1.0_all.snap | grep -Pq "$expected"
+ expected="^test-snapd-devmode +.* +devmode"
+ snap list | grep -Pq "$expected"
+
+ echo "Sideload devmode snap succeeds with --jailmode"
+ expected="test-snapd-devmode 1.0 installed"
+ snap install --dangerous --jailmode ./test-snapd-devmode_1.0_all.snap | grep -Pq "$expected"
+ expected="^test-snapd-devmode +.* +jailmode"
+ snap list | grep -Pq "$expected"
+
+ echo "Sideload devmode snap fails with both --devmode and --jailmode"
+ expected="cannot use devmode and jailmode flags together"
+ ( snap install --devmode --jailmode ./test-snapd-devmode_1.0_all.snap 2>&1 || true ) | grep -Pzq "$expected"
+
+ echo "Sideload a second time succeeds"
+ snap install --dangerous ./test-snapd-tools_1.0_all.snap
+ test-snapd-tools.success
+
+ # TODO: check we copy the data directory over
+
+ echo "Remove --revision works"
+ snap remove --revision x1 test-snapd-tools
+ test-snapd-tools.success
+ test ! -d /snap/test-snapd-tools/x1
--- /dev/null
+summary: snap install a large snap from the store (bigger than tmpfs)
+
+prepare: |
+ systemctl stop snapd.{service,socket}
+ mount -t tmpfs -o rw,nosuid,nodev,size=4 none /tmp
+ systemctl start snapd.{socket,service}
+
+restore: |
+ systemctl stop snapd.{service,socket}
+ umount /tmp || true
+ systemctl start snapd.{socket,service}
+
+execute: |
+ # test-snapd-tools is about 8k, tmpfs is 4k :-)
+ snap install test-snapd-tools
+ snap remove test-snapd-tools
--- /dev/null
+summary: Checks for special cases of snap install from the store
+
+systems: [+ubuntu-core-16-64]
+
+environment:
+ SNAP_NAME: test-snapd-tools
+ DEVMODE_SNAP: test-snapd-devmode
+ # Ensure that running purely from the deb (without re-exec) works
+ # correctly
+ SNAP_REEXEC/reexec0: 0
+ SNAP_REEXEC/reexec1: 1
+
+execute: |
+ echo "Install from different channels"
+ expected="(?s)$SNAP_NAME .* from 'canonical' installed\n"
+ for channel in edge beta candidate stable
+ do
+ snap install $SNAP_NAME --channel=$channel | grep -Pzq "$expected"
+ snap remove $SNAP_NAME
+ done
+
+ echo "Install non-devmode snap with devmode option"
+ expected="(?s)$SNAP_NAME .* from 'canonical' installed\n"
+ snap install $SNAP_NAME --devmode | grep -Pzq "$expected"
+
+ echo "Install devmode snap without devmode option"
+ # XXX want to move this to a more precise, verbose, user-friendly
+ # error (e.g. "snap asks for devmode but not provided nor
+ # overridden")
+ expected="snap not found"
+ actual=$(snap install --channel beta $DEVMODE_SNAP 2>&1 && exit 1 || true)
+ echo "$actual" | grep -Pzq "$expected"
+
+ echo "Install devmode snap from stable"
+ expected="snap not found"
+ actual=$(snap install --devmode $DEVMODE_SNAP 2>&1 && exit 1 || true)
+ echo "$actual" | grep -Pzq "$expected"
+
+ echo "Install devmode snap from beta with devmode option"
+ expected="(?s)$DEVMODE_SNAP .*"
+ actual=$(snap install --channel beta --devmode $DEVMODE_SNAP)
+ echo "$actual" | grep -Pzq "$expected"
--- /dev/null
+summary: Ensure bluez interface works.
+
+details: |
+ The bluez interface allows the bluez service to run and clients to
+ communicate with it.
+
+ This test verifies the the bluez snap from the store installs and
+ we can connect its slot and plug.
+
+environment:
+ SNAP_NAME: bluez
+
+execute: |
+ echo "Installing bluez snap from the store ..."
+ expected="(?s)$SNAP_NAME .* from 'canonical' installed\n"
+ snap install $SNAP_NAME | grep -Pzq "$expected"
+
+ echo "Connecting bluez snap plugs/slots ..."
+ snap connect bluez:client bluez:service
--- /dev/null
+summary: Check the interfaces command
+
+environment:
+ SNAP_NAME: network-consumer
+ SNAP_FILE: ${SNAP_NAME}_1.0_all.snap
+ PLUG: network
+
+prepare: |
+ echo "Given a snap with the $PLUG plug is installed"
+ snapbuild $TESTSLIB/snaps/$SNAP_NAME .
+ snap install --dangerous $SNAP_FILE
+
+restore: |
+ rm -f $SNAP_FILE
+
+execute: |
+ expected="(?s)Slot +Plug\n\
+ :$PLUG +$SNAP_NAME"
+
+ echo "When the interfaces list is restricted by slot"
+ echo "Then only the requested slots are shown"
+ snap interfaces -i $PLUG | grep -Pzq "$expected"
+
+ echo "==============================================="
+
+ echo "When the interfaces list is restricted by slot and snap"
+ echo "Then only the requested slots are shown"
+ snap interfaces -i $PLUG $SNAP_NAME | grep -Pzq "$expected"
--- /dev/null
+summary: Ensure that the content sharing interface works.
+
+details: |
+ The content-sharing interface interface allows a snap to access contents from
+ other snap
+
+ A snap which defines the content sharing plug must be shown in the interfaces list.
+ The plug must be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+prepare: |
+ echo "Given a snap declaring a content sharing slot is installed"
+ snap install --edge test-snapd-content-slot
+
+ echo "And a snap declaring a content sharing plug is installed"
+ snap install --edge test-snapd-content-plug
+
+execute: |
+ CONNECTED_PATTERN="test-snapd-content-slot:shared-content-slot +test-snapd-content-plug:shared-content-plug"
+ DISCONNECTED_PATTERN="(?s).*?test-snapd-content-slot:shared-content-slot +-.*?- +test-snapd-content-plug:shared-content-plug"
+
+ echo "Then the snap is listed as connected"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "And fstab files are created"
+ [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ]
+
+ echo "And we can use the shared content"
+ test-snapd-content-plug.content-plug | grep "Some shared content"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the fstab files are removed"
+ [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -eq 0 ]
+
+ echo "When the plug is reconnected"
+ snap connect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the fstab files are recreated"
+ [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ]
--- /dev/null
+summary: Ensure that the cups interface works.
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+details: |
+ The cups-control interface allows a snap to access the locale configuration.
+
+ A snap which defines the cups-control plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to talk to the cups daemon.
+ The test snap used pulls the lpr functionality and installs a pdf printer. In order
+ to actually connect to the socket it needs to declare a plug on the network interface.
+ The test checks for the existence of a pdf file generated by the lpr command in the
+ snap.
+
+environment:
+ TEST_FILE: /var/snap/test-snapd-cups-control-consumer/current/test_file.txt
+
+prepare: |
+ echo "Given a snap declaring a cups plug is installed"
+ snap install test-snapd-cups-control-consumer
+
+ echo "And the pdf printer is available"
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ apt-get install -y cups-pdf
+ else
+ apt-get install -y printer-driver-cups-pdf
+ fi
+
+restore: |
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ apt-get remove -y cups-pdf
+ else
+ apt-get remove -y printer-driver-cups-pdf
+ fi
+ rm -rf $HOME/PDF $TEST_FILE print.error
+
+execute: |
+ CONNECTED_PATTERN=":cups-control +test-snapd-cups-control-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-cups-control-consumer:cups-control"
+ . "$TESTSLIB/names.sh"
+
+ echo "Then it is not shown as connected"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "===================================="
+
+ echo "When the plug is connected"
+ snap connect test-snapd-cups-control-consumer:cups-control ${core_name}:cups-control
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap command is able to print files"
+ echo "Hello World" > $TEST_FILE
+ test-snapd-cups-control-consumer.lpr $TEST_FILE
+ while ! test -e $HOME/PDF/test_file.pdf; do sleep 1; done
+
+ echo "===================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect test-snapd-cups-control-consumer:cups-control ${core_name}:cups-control
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap command is not able to print files"
+ if test-snapd-cups-control-consumer.lpr $TEST_FILE 2>print.error; then
+ echo "Expected error with plug disconnected"
+ exit 1
+ fi
+ grep -q "scheduler not responding" print.error
--- /dev/null
+summary: Ensure that the firewall-control interface works.
+
+details: |
+ The firewall-control interface allows a snap to configure the firewall.
+
+ A snap which defines the firewall-control plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ For this test we use a snap that declares a plug on this interface and that adds and
+ removes iptables entries. With the plug connected the test checks that a rule to map
+ localhost to a given IP can be added by the snap, ensuring that a generic client can
+ access a generic service listening on localhost through the IP set up in the firewall
+ rule.
+
+environment:
+ PORT: 8081
+ SERVICE_FILE: "./service.sh"
+ SERVICE_NAME: "test-service"
+ REQUEST_FILE: "./request.txt"
+ DESTINATION_IP: "172.26.0.15"
+
+prepare: |
+ echo "Given a snap declaring a plug on the firewall-control interface is installed"
+ snapbuild $TESTSLIB/snaps/firewall-control-consumer .
+ snap install --dangerous firewall-control-consumer_1.0_all.snap
+
+ echo "And a service is listening"
+ printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE
+ chmod a+x $SERVICE_FILE
+ . "$TESTSLIB/systemd.sh"
+ systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)"
+
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+ echo "And we store a basic HTTP request"
+ cat > $REQUEST_FILE <<EOF
+ GET / HTTP/1.0
+
+ EOF
+
+restore: |
+ . "$TESTSLIB/systemd.sh"
+ systemd_stop_and_destroy_unit $SERVICE_NAME
+ rm -f firewall-control-consumer_1.0_all.snap firewall-create.error $SERVICE_FILE $REQUEST_FILE
+
+execute: |
+ CONNECTED_PATTERN=":firewall-control +firewall-control-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +firewall-control-consumer:firewall-control"
+ . "$TESTSLIB/names.sh"
+
+ echo "Then it is not connected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect firewall-control-consumer:firewall-control ${core_name}:firewall-control
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "And the snap creates a firewall rule"
+ firewall-control-consumer.create
+
+ echo "Then the service listening on localhost is accessible through the destination IP in the rule"
+ nc -w 2 "$DESTINATION_IP" "$PORT" < $REQUEST_FILE | grep -Pqz "ok\n"
+
+ echo "When the snap deletes the firewall rule"
+ firewall-control-consumer.delete
+
+ echo "Then the service listening on localhost is no longer accessible through the destination IP in the rule"
+ ! nc -w 2 "$DESTINATION_IP" "$PORT" < $REQUEST_FILE
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect firewall-control-consumer:firewall-control ${core_name}:firewall-control
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap is not able to configure the firewall"
+ if firewall-control-consumer.create 2>${PWD}/firewall-create.error; then
+ echo "Expected permission error creating firewall rules with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" firewall-create.error
--- /dev/null
+summary: Ensure that the fuse-support interface works.
+
+# no support for fuse on 14.04
+systems: [-ubuntu-14.04-64, -ubuntu-14.04-32]
+
+details: |
+ The fuse-support interface allows a snap to manage FUSE file systems.
+
+ A snap which defines the fuse-support plug must be shown in the interfaces list.
+ The plug must be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to create a fuse filesystem
+ in a writable zone. The fuse-consumer test snap creates a readable file with a known
+ name and content in the mount point given to the command.
+
+environment:
+ MOUNT_POINT: /var/snap/test-snapd-fuse-consumer/current/mount_point
+
+prepare: |
+ echo "Given a snap declaring a fuse plug is installed"
+ snap install test-snapd-fuse-consumer
+
+ echo "And a user writable mount point is created"
+ mkdir -p $MOUNT_POINT
+
+restore: |
+ umount $MOUNT_POINT || true
+ rm -rf $MOUNT_POINT fuse.error
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN=":fuse-support +test-snapd-fuse-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-fuse-consumer:fuse-support"
+
+ echo "Then the fuse plug is not connected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap is not able to create a fuse file system"
+ if test-snapd-fuse-consumer.create $MOUNT_POINT 2>${PWD}/fuse.error; then
+ echo "Expected permission error creating fuse filesystem with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" fuse.error
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect test-snapd-fuse-consumer:fuse-support ${core_name}:fuse-support
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to create a fuse filesystem"
+ test-snapd-fuse-consumer.create $MOUNT_POINT
+ PID=$(ps aux | grep test-snapd.*create | grep -v grep | awk '{print $2}')
+ grep -q "Hello World!" /proc/${PID}/root/$MOUNT_POINT/hello
+ kill -9 ${PID}
--- /dev/null
+summary: Ensure that the hardware-observe interface works.
+
+summary: |
+ The hardware-observe interface allows a snap to access hardware information.
+
+ A snap which defines the hardware-observe plug must be shown in the interfaces list.
+ The plug must not be connected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to read files in /sys/{block,bus,class,devices}
+
+prepare: |
+ echo "Given a snap declaring a plug on the hardware-observe interface is installed"
+ snapbuild $TESTSLIB/snaps/hardware-observe-consumer .
+ snap install --dangerous hardware-observe-consumer_1.0_all.snap
+
+restore: |
+ rm -f hardware-observe-consumer_1.0_all.snap hw.error
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN=":hardware-observe +hardware-observe-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +hardware-observe-consumer:hardware-observe"
+
+ echo "Then it is not connected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect hardware-observe-consumer:hardware-observe ${core_name}:hardware-observe
+
+ echo "Then the snap is able to read hardware information"
+ hardware-observe-consumer.consumer
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect hardware-observe-consumer:hardware-observe ${core_name}:hardware-observe
+
+ echo "Then the snap is not able to read the hardware information"
+ if hardware-observe-consumer.consumer 2>hw.error; then
+ echo "Expected permission error accessing locale configuration with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" hw.error
--- /dev/null
+summary: Ensure that the home interface works.
+
+details: |
+ The home interface allows a snap to access non-hidden files in $HOME
+
+ A snap which defines the home plug must be shown in the interfaces list.
+ The plug must be autoconnected on install for classic systems and disconnected
+ on all-snaps and, as usual, must be able to be reconnected. When connected
+ it must grant access to non hidden home files.
+
+environment:
+ SNAP_FILE: "home-consumer_1.0_all.snap"
+ CREATABLE_FILE: "$HOME/creatable"
+ READABLE_FILE: "$HOME/readable"
+ WRITABLE_FILE: "$HOME/writable"
+ HIDDEN_CREATABLE_FILE: "$HOME/.creatable"
+ HIDDEN_READABLE_FILE: "$HOME/.readable"
+
+prepare: |
+ echo "Given a snap declaring the home plug is installed"
+ snapbuild $TESTSLIB/snaps/home-consumer .
+ snap install --dangerous $SNAP_FILE
+
+ echo "And there is a readable file in HOME"
+ echo ok > "$READABLE_FILE"
+
+ echo "And there is a writable file in HOME"
+ echo ok > "$WRITABLE_FILE"
+
+ echo "And there is a hidden readable file in HOME"
+ echo ok > "$HIDDEN_READABLE_FILE"
+
+restore: |
+ rm -f $SNAP_FILE $READABLE_FILE $WRITABLE_FILE $CREATABLE_FILE $HIDDEN_READABLE_FILE
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ :home +home-consumer"
+ DISCONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ - +home-consumer:home"
+
+ if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then
+ echo "Then the snap is listed as disconnected"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "And the plug can be connected"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+ else
+ echo "Then the snap is listed as connected"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the plug can be connected again"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+ fi
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to read home files"
+ home-consumer.reader $READABLE_FILE | grep -Pqz ok
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then snap can't read home files"
+ if home-consumer.reader $READABLE_FILE; then
+ echo "Home files shouldn't be readable" && exit 1
+ fi
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to append to home files"
+ home-consumer.writer "$WRITABLE_FILE"
+ cat "$WRITABLE_FILE" | grep -Pqz "ok\nok"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then snap can't append to home files"
+ if home-consumer.writer "$WRITABLE_FILE"; then
+ echo "Home files shouldn't be writable" && exit 1
+ fi
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to create home files"
+ home-consumer.writer "$CREATABLE_FILE"
+ cat "$CREATABLE_FILE" | grep -Pqz "ok"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then snap can't create home files"
+ if home-consumer.writer "$CREATABLE_FILE"; then
+ echo "It should be impossible to create home files" && exit 1
+ fi
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is not able to read hidden home files"
+ if home-consumer.reader "$HIDDEN_READABLE_FILE"; then
+ echo "Hidden home files shouldn't be readable" && exit 1
+ fi
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is not able to write hidden home files"
+ if home-consumer.writer "$HIDDEN_CREATABLE_FILE"; then
+ echo "It should be impossible to create hidden home files" && exit 1
+ fi
--- /dev/null
+summary: Check that IIO device nodes are accessible through an interface
+
+details: |
+ This test makes sure that a snap using the IIO interface can access
+ devices nodes exposed by a slot properly.
+
+ It modifies the core snap to provide a iio slot. The actual iio device
+ node is served as a plain file with static text content. The test expects
+ that, after a snap declared a iio plug is installed and connected, it can
+ access the node and read/write its content.
+
+systems: [ubuntu-core-16-64]
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ # Mock IIO device node and give it some content we can verify
+ # the test snap can read.
+ echo "iio-0" > /dev/iio:device0
+
+ echo "Given a snap declaring a plug on iio is installed"
+ . $TESTSLIB/snaps.sh
+ install_local iio-consumer
+
+ echo "And the iio plug is connected"
+ . "$TESTSLIB/names.sh"
+ snap connect iio-consumer:iio ${core_name}:iio0
+
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ rm /dev/iio:device0
+
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ test "`/snap/bin/iio-consumer.read`" = "iio-0"
+
+ /snap/bin/iio-consumer.write "hello"
+ test "`/snap/bin/iio-consumer.read`" = "hello"
--- /dev/null
+summary: Ensure that the locale-control interface works.
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+summary: |
+ The locale-control interface allows a snap to access the locale configuration.
+
+ A snap which defines the locale-control plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to access the /etc/default/locale
+ file both for reading and writing.
+
+prepare: |
+ echo "Given a snap declaring a plug on the locale-control interface is installed"
+ snapbuild $TESTSLIB/snaps/locale-control-consumer .
+ snap install --dangerous locale-control-consumer_1.0_all.snap
+ mv /etc/default/locale locale.back
+ cat > /etc/default/locale <<EOF
+ LANG="$LANG"
+ LANGUAGE="$LANGUAGE"
+ EOF
+
+restore: |
+ rm -f locale-control-consumer_1.0_all.snap locale-read.error locale-write.error
+ mv locale.back /etc/default/locale
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN=":locale-control +locale-control-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +locale-control-consumer:locale-control"
+
+ echo "Then it is not connected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect locale-control-consumer:locale-control ${core_name}:locale-control
+
+ echo "Then the snap is able to read the locale configuration"
+ [ "$(su -l -c 'locale-control-consumer.get LANG' test)" = "$LANG" ]
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect locale-control-consumer:locale-control ${core_name}:locale-control
+
+ echo "Then the snap is not able to read the locale configuration"
+ if su -l -c "locale-control-consumer.get LANG 2>${PWD}/locale-read.error" test; then
+ echo "Expected permission error accessing locale configuration with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" locale-read.error
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect locale-control-consumer:locale-control ${core_name}:locale-control
+
+ echo "Then the snap is able to write the locale configuration"
+ locale-control-consumer.set LANG mylang
+ grep -q "LANG=\"mylang\"" /etc/default/locale
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect locale-control-consumer:locale-control ${core_name}:locale-control
+
+ echo "Then the snap is not able to read the locale configuration"
+ if locale-control-consumer.set LANG mysecondlang 2>${PWD}/locale-write.error; then
+ echo "Expected permission error accessing locale configuration with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" locale-write.error
--- /dev/null
+summary: Check that the log-observe interface works.
+
+details: |
+ The log-observe interface allows a snap to read system logs and set kernel
+ log rate-limiting.
+
+ A snap which defines the log-observe plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+environment:
+ SNAP_NAME: log-observe-consumer
+ SNAP_FILE: "${SNAP_NAME}_1.0_all.snap"
+ PLUG: log-observe
+
+prepare: |
+ echo "Given a snap declaring the $PLUG plug is installed"
+ snapbuild $TESTSLIB/snaps/$SNAP_NAME .
+ snap install --dangerous $SNAP_FILE
+
+restore: |
+ rm -f $SNAP_FILE
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ :$PLUG +$SNAP_NAME"
+ DISCONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ - +$SNAP_NAME:$PLUG"
+
+ echo "Then the snap is not listed as connected"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect $SNAP_NAME:$PLUG ${core_name}:$PLUG
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the plug can be disconnected again"
+ snap disconnect $SNAP_NAME:$PLUG ${core_name}:$PLUG
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect $SNAP_NAME:$PLUG ${core_name}:$PLUG
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to access the system logs"
+ log-observe-consumer | grep -Pqz "ok\n"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect $SNAP_NAME:$PLUG ${core_name}:$PLUG
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then snap can't access the system logs"
+ if log-observe-consumer; then
+ echo "System log shouldn't be accessible"
+ exit 1
+ fi
--- /dev/null
+summary: Ensures that the mount-observe interface works
+
+details: |
+ A snap declaring the mount-observe plug is defined, its command
+ just read the /proc/<pid>/mounts file.
+
+ The test itself checks for the lack of autoconnect and then tries
+ to execute the snap command with the plug connected (it must succeed)
+ and disconnected (it must fail).
+
+ The test also checks that a new mount created after the snap is installed
+ is also shown when the plug is connected.
+
+prepare: |
+ echo "Given a snap declaring a plug on the mount-observe interface is installed"
+ snapbuild $TESTSLIB/snaps/mount-observe-consumer .
+ snap install --dangerous mount-observe-consumer_1.0_all.snap
+
+restore: |
+ rm -f mount-observe-consumer_1.0_all.snap
+
+execute: |
+ . "$TESTSLIB/names.sh"
+ CONNECTED_PATTERN=":mount-observe +mount-observe-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +mount-observe-consumer:mount-observe"
+
+ echo "Then the plug is shown as disconnected"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "========================================="
+
+ echo "When the plug is connected"
+ snap connect mount-observe-consumer:mount-observe ${core_name}:mount-observe
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the mount info is reachable"
+ expected="/snap/mount-observe-consumer"
+ su -l -c "mount-observe-consumer" test | grep -Pq "$expected"
+
+ echo "========================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect mount-observe-consumer:mount-observe ${core_name}:mount-observe
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the mount info is not reachable"
+ if su -l -c "mount-observe-consumer" test; then
+ echo "Expected error accessing mount info with disconnected plug"
+ exit 1
+ fi
+
+ echo "========================================="
+
+ echo "When the plug is connected"
+ snap connect mount-observe-consumer:mount-observe ${core_name}:mount-observe
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "And a new mount is created"
+ . "$TESTSLIB/snaps.sh"
+ install_local test-snapd-tools
+
+ echo "Then the new mount info is reachable"
+ expected="/snap/test-snapd-tools"
+ su -l -c "mount-observe-consumer" test | grep -Pq "$expected"
--- /dev/null
+summary: Ensure that the network-bind interface works
+
+details: |
+ The network-bind interface allows a daemon to access the network as a server.
+
+ A snap which defines the network-bind plug must be shown in the interfaces list.
+ The plug must be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be accessible by a network client.
+
+environment:
+ SNAP_NAME: network-bind-consumer
+ SNAP_FILE: ${SNAP_NAME}_1.0_all.snap
+ PORT: 8081
+ REQUEST_FILE: ./request.txt
+
+prepare: |
+ echo "Given a snap declaring the network-bind plug is installed"
+ snapbuild $TESTSLIB/snaps/$SNAP_NAME .
+ snap install --dangerous $SNAP_FILE
+
+ echo "Given the snap's service is listening"
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+ echo "Given we store a basic HTTP request"
+ cat > $REQUEST_FILE <<EOF
+ GET / HTTP/1.0
+
+ EOF
+
+restore: |
+ rm -f $SNAP_FILE $REQUEST_FILE
+
+execute: |
+ . "$TESTSLIB/names.sh"
+ CONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ :network-bind +$SNAP_NAME"
+ DISCONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ - +$SNAP_NAME:network-bind"
+
+ echo "Then the snap is listed as connected"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect $SNAP_NAME:network-bind ${core_name}:network-bind
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the plug can be connected again"
+ snap connect $SNAP_NAME:network-bind ${core_name}:network-bind
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect $SNAP_NAME:network-bind ${core_name}:network-bind
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the service is accessible by a client"
+ nc -w 2 -q 2 localhost "$PORT" < $REQUEST_FILE | grep -Pqz "ok\n"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect $SNAP_NAME:network-bind ${core_name}:network-bind
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the service is not accessible by a client"
+ response=$(nc -w 2 -q 2 localhost "$PORT" < $REQUEST_FILE)
+ [ "$response" = "" ]
--- /dev/null
+summary: The /run/netns directory propagates events outwards
+details: |
+ The /run/netns directory is special in that mount events propagate outward
+ from the mount namespace used by snap applications into the main mount
+ namespace.
+prepare: |
+ snap install --devmode snapd-hacker-toolbelt
+ echo "Workaround complain-mode EPERM (LP: #1648903)"
+ sed -i 's#^}#/{,usr/}{,s}bin/ip ixr,\n}#' /var/lib/snapd/apparmor/profiles/snap.snapd-hacker-toolbelt.busybox
+ apparmor_parser -r /var/lib/snapd/apparmor/profiles/snap.snapd-hacker-toolbelt.busybox
+execute: |
+ export PATH=$PATH:/snap/bin
+ test ! -e /run/netns/canary
+ echo "Network namespace created within a snap exists in global"
+ snapd-hacker-toolbelt.busybox sh -c '/bin/ip netns add canary'
+ test -e /run/netns/canary
+ ip netns list | grep canary
+ echo "Network namespace deleted from global is removed from snap"
+ ip netns delete canary
+ test ! -e /run/netns/canary
+ snapd-hacker-toolbelt.busybox sh -c '/bin/ip netns list' | grep canary && false
+ echo "Network namespace created from global exists in snap"
+ ip netns add canary
+ test -e /run/netns/canary
+ snapd-hacker-toolbelt.busybox sh -c '/bin/ip netns list' | grep canary
+ echo "Network namespace deleted from snap is removed from global"
+ snapd-hacker-toolbelt.busybox sh -c '/bin/ip netns delete canary'
+ test ! -e /run/netns/canary
+restore: |
+ snap remove snapd-hacker-toolbelt
+ # If this doesn't work maybe it is because the test didn't execute correctly
+ snapd-hacker-toolbelt.busybox sh -c '/bin/ip netns delete canary' 2>/dev/null || true
+ ip netns delete canary 2>/dev/null || true
--- /dev/null
+summary: Ensure that the network-control interface works.
+
+details: |
+ The network-control interface allows a snap to configure networking.
+
+ A snap which defines the network-control plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to modify the network configuration
+ and ask for its status, the test sets up a network service, gets information about it (read
+ capability) and creates an arp entry (write capability).
+
+environment:
+ PORT: 8081
+ SERVICE_FILE: "./service.sh"
+ SERVICE_NAME: "test-service"
+ ARP_ENTRY_ADDR: "30.30.30.30"
+
+prepare: |
+ . "$TESTSLIB/systemd.sh"
+
+ echo "Given a snap declaring a plug on the network-control interface is installed"
+ snapbuild $TESTSLIB/snaps/network-control-consumer .
+ snap install --dangerous network-control-consumer_1.0_all.snap
+
+ echo "And a network service is up"
+ printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE
+ chmod a+x $SERVICE_FILE
+ systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)"
+
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+restore: |
+ . "$TESTSLIB/systemd.sh"
+ . "$TESTSLIB/network.sh"
+
+ systemd_stop_and_destroy_unit $SERVICE_NAME
+ rm -f network-control-consumer_1.0_all.snap net-query.output net-command.output $SERVICE_FILE
+ arp -d $ARP_ENTRY_ADDR -i $(get_default_iface) || true
+
+execute: |
+ . "$TESTSLIB/names.sh"
+ . "$TESTSLIB/network.sh"
+
+ CONNECTED_PATTERN=":network-control +network-control-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +network-control-consumer:network-control"
+ INTERFACE=$(get_default_iface)
+
+ echo "Then the plug disconnected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "===================================="
+
+ echo "When the plug is connected"
+ snap connect network-control-consumer:network-control ${core_name}:network-control
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap command can query network status information"
+ network-control-consumer.query | grep -P "0.0.0.0:$PORT.*?LISTEN"
+
+ echo "===================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect network-control-consumer:network-control ${core_name}:network-control
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap command can not query network status information"
+ if network-control-consumer.query 2>net-query.output; then
+ echo "Expected error caling command with disconnected plug"
+ fi
+ cat net-query.output | grep -Pq "Permission denied"
+
+ echo "===================================="
+
+ echo "When the plug is connected"
+ snap connect network-control-consumer:network-control ${core_name}:network-control
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap command can modify the network configuration"
+ network-control-consumer.add-arp-entry $ARP_ENTRY_ADDR $INTERFACE
+ expected="(?s)br0.*?state UP.*?bridge.*?foo@bar.*?veth.*?bar@foo.*?veth"
+ arp | grep -Pq "$ARP_ENTRY_ADDR.*?ether.*?CM"
+
+ echo "===================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect network-control-consumer:network-control ${core_name}:network-control
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap command can not modify the network configuration"
+ if network-control-consumer.add-arp-entry $ARP_ENTRY_ADDR $INTERFACE 2>net-command.output; then
+ echo "Expected error caling command with disconnected plug"
+ fi
+ cat net-command.output | grep -Pq "Permission denied"
--- /dev/null
+summary: Ensure that the network-observe interface works
+
+details: |
+ The network-observe interface allows a snap to query the network status information.
+
+ A snap which defines the network-observe plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to access read the network status,
+ the test sets up a network service to establish a known state in the network to be queried.
+
+environment:
+ PORT: 8081
+ SERVICE_FILE: "./service.sh"
+ SERVICE_NAME: "test-service"
+
+prepare: |
+ . "$TESTSLIB/systemd.sh"
+
+ echo "Given a snap declaring a plug on the network-observe interface is installed"
+ snapbuild $TESTSLIB/snaps/network-observe-consumer .
+ snap install --dangerous network-observe-consumer_1.0_all.snap
+
+ echo "And a network service is up"
+ printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE
+ chmod a+x $SERVICE_FILE
+ systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)"
+
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+restore: |
+ . "$TESTSLIB/systemd.sh"
+
+ systemd_stop_and_destroy_unit $SERVICE_NAME
+ rm -f network-observe-consumer_1.0_all.snap net-query.output $SERVICE_FILE
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN=":network-observe +network-observe-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +network-observe-consumer:network-observe"
+
+ echo "Then the plug disconnected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "===================================="
+
+ echo "When the plug is connected"
+ snap connect network-observe-consumer:network-observe ${core_name}:network-observe
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap command can query network status information"
+ network-observe-consumer | grep -P "0.0.0.0:$PORT.*?LISTEN"
+
+ echo "===================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect network-observe-consumer:network-observe ${core_name}:network-observe
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap command can not query network status information"
+ if network-observe-consumer 2>net-query.output; then
+ echo "Expected error caling command with disconnected plug"
+ fi
+ cat net-query.output | grep -Pq "Permission denied"
--- /dev/null
+summary: Ensure network interface works.
+
+details: |
+ The network interface allows a snap to access the network as a client.
+
+ A snap which defines the network plug must be shown in the interfaces list.
+ The plug must be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to access network services.
+
+environment:
+ SNAP_NAME: network-consumer
+ SNAP_FILE: ${SNAP_NAME}_1.0_all.snap
+ PORT: 8081
+ SERVICE_FILE: "./service.sh"
+ SERVICE_NAME: "test-service"
+
+prepare: |
+ . $TESTSLIB/systemd.sh
+ echo "Given a snap declaring the network plug is installed"
+ snapbuild $TESTSLIB/snaps/$SNAP_NAME .
+ snap install --dangerous $SNAP_FILE
+
+ echo "And a service is listening"
+ printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE
+ chmod a+x $SERVICE_FILE
+ systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)"
+
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+restore: |
+ . $TESTSLIB/systemd.sh
+ systemd_stop_and_destroy_unit $SERVICE_NAME
+ rm -f $SNAP_FILE $SERVICE_FILE
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ :network +$SNAP_NAME"
+ DISCONNECTED_PATTERN="(?s)Slot +Plug\n\
+ .*?\n\
+ - +$SNAP_NAME:network"
+
+ echo "Then the snap is listed as connected"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect $SNAP_NAME:network ${core_name}:network
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the plug can be connected again"
+ snap connect $SNAP_NAME:network ${core_name}:network
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "============================================"
+
+ echo "When the plug is connected"
+ snap connect $SNAP_NAME:network ${core_name}:network
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to access a network service"
+ network-consumer http://127.0.0.1:$PORT | grep -Pqz "ok\n"
+
+ echo "============================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect $SNAP_NAME:network ${core_name}:network
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then snap can't access a network service"
+ if network-consumer http://127.0.0.1:$PORT; then
+ echo "Network shouldn't be accessible"
+ exit 1
+ fi
--- /dev/null
+summary: Ensure that the process-control interface works.
+
+summary: |
+ The process-control interface allows a snap to control other processes via signals
+ and nice.
+
+ A snap which defines the process-control plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to kill other processes. Currently
+ this test does not check the priority change capability of the interface, will be
+ extended later.
+
+prepare: |
+ echo "Given a snap declaring a plug on the process-control interface is installed"
+ snapbuild $TESTSLIB/snaps/process-control-consumer .
+ snap install --dangerous process-control-consumer_1.0_all.snap
+
+restore: |
+ rm -f process-control-consumer_1.0_all.snap process-kill.error process-nice.error
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN=":process-control +process-control-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +process-control-consumer:process-control"
+
+ echo "Then it is not connected by default"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect process-control-consumer:process-control ${core_name}:process-control
+
+ echo "Then the snap is able to kill an existing process"
+ while :; do sleep 1; done &
+ pid=$!
+ ps ax | grep -Pq "^ *$pid"
+ process-control-consumer.signal "SIGTERM" $pid
+ ! ps ax | grep -Pq "^ *$pid"
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect process-control-consumer:process-control ${core_name}:process-control
+
+ echo "Then the snap is not able to kill an existing process"
+ while :; do sleep 1; done &
+ pid=$!
+ if process-control-consumer.signal SIGTERM $pid 2>${PWD}/process-kill.error; then
+ echo "Expected permission error accessing killing a process with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" process-kill.error
+ ps ax | grep -Pq "^ *$pid"
+ kill -9 $pid
+ ! ps ax | grep -Pq "^ *$pid"
--- /dev/null
+summary: Ensure that the snapd-control interface works.
+
+details: |
+ The snapd-control interface allows a snap to access the locale configuration.
+
+ A snap which defines the snapd-control plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to control the snapd daemon
+ through the socket, the test snap used has a command to install a snap (exercising
+ the write capability on the socket) and a command to list the installed snaps (which
+ checks the read capability). A network plug must be defined and connected for the
+ snap to be able to talk to the socket, the snapd-control is not enough by itself.
+
+
+prepare: |
+ echo "Given a snap declaring a plug on the snapd-control interface is installed"
+ snap install --edge test-snapd-control-consumer
+
+restore: |
+ rm -f snapd.error
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ CONNECTED_PATTERN=":snapd-control +test-snapd-control-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-control-consumer:snapd-control"
+
+ echo "Then it is connected by default"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "================================================"
+
+ echo "When the plug is disconnected"
+ snap disconnect test-snapd-control-consumer:snapd-control ${core_name}:snapd-control
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap command is not able to control snapd"
+ if test-snapd-control-consumer.list 2>snapd.error; then
+ echo "Expected error with plug disconnected"
+ exit 1
+ fi
+ grep -q "Permission denied" snapd.error
+
+ echo "================================================"
+
+ echo "When the plug is connected"
+ snap connect test-snapd-control-consumer:snapd-control ${core_name}:snapd-control
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap command is able to control snapd"
+ ! test-snapd-control-consumer.list | grep -q test-snapd-tools
+ test-snapd-control-consumer.install test-snapd-tools
+ while ! test-snapd-control-consumer.list | grep -q test-snapd-tools; do sleep 1; done
--- /dev/null
+summary: Ensures that the system-observe interface works.
+
+details: |
+ A snap declaring the system-observe plug is defined, its command
+ just calls ps -ax.
+
+ The test itself checks for the lack of autoconnect and then tries
+ to execute the snap command with the plug connected (it must succeed)
+ and disconnected (it must fail).
+
+prepare: |
+ echo "Given a snap declaring a plug on the system-observe interface is installed"
+ snapbuild $TESTSLIB/snaps/system-observe-consumer .
+ snap install --dangerous system-observe-consumer_1.0_all.snap
+
+restore: |
+ rm -f system-observe-consumer_1.0_all.snap
+
+execute: |
+ . "$TESTSLIB/names.sh"
+ CONNECTED_PATTERN=":system-observe +system-observe-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +system-observe-consumer:system-observe"
+
+ echo "Then the plug is shown as disconnected"
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "==========================================="
+
+ echo "When the plug is connected"
+ snap connect system-observe-consumer:system-observe ${core_name}:system-observe
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to get system information"
+ expected="(?s)/dev/tty.*?serial"
+ su -l -c "system-observe-consumer" test | grep -Pq "$expected"
+
+ echo "==========================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect system-observe-consumer:system-observe ${core_name}:system-observe
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap is not able to get system information"
+ if su -l -c "system-observe-consumer" test; then
+ echo "Expected error with plug disconnected"
+ exit 1
+ fi
--- /dev/null
+summary: Check that RTC device nodes are accessible through an interface
+
+details: |
+ This test makes sure that a snap using the time-control interface
+ can access the /dev/rtc device node exposed by a slot on the OS
+ snap properly.
+
+systems: [ubuntu-core-16-64]
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ echo "Given a snap declaring a plug on time-control is installed"
+ . $TESTSLIB/snaps.sh
+ install_local time-control-consumer
+
+ echo "And the time-control plug is connected"
+ . $TESTSLIB/names.sh
+ snap connect time-control-consumer:time-control :time-control
+
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ # Read/write access should be possible
+ test -n "`/snap/bin/time-control-consumer.read`"
+ /snap/bin/time-control-consumer.write
--- /dev/null
+summary: Ensure that the udev interface backend works.
+
+details: |
+ This test checks that the udev rules file is created when a snap declaring
+ a dependency on an interface (being it a slot or a plug) and that concrete
+ dependency has a related udev snippet, then the udev rules files are created
+ on install and removed after it is uninstalled.
+
+ Currently the ony interface that declares a udev snippet is the modem-manager
+ interface for its slot. This test can be easily extended with variants when
+ more interfaces declare udev snippets.
+
+prepare: |
+ echo "Given a snap declaring a slot with associated udev rules is installed"
+ snapbuild $TESTSLIB/snaps/modem-manager-consumer .
+ snap install --dangerous modem-manager-consumer_1.0_all.snap
+
+restore: |
+ rm -f modem-manager-consumer_1.0_all.snap
+
+execute: |
+ echo "Then the udev rules files specific to it are created"
+ test -f /etc/udev/rules.d/70-snap.modem-manager-consumer.rules
+ expected="ATTRS{idVendor}==\".*?\", ATTRS{idProduct}==\".*?\""
+ grep -Pq "$expected" /etc/udev/rules.d/70-snap.modem-manager-consumer.rules
+
+ echo "======================================="
+
+ echo "When the snap is removed"
+ snap remove modem-manager-consumer
+
+ echo "Then the udev rules files are removed"
+ ! test -f /etc/udev/rules.d/70-snap.modem-manager-consumer.rules
--- /dev/null
+summary: Ensure that the upower-observe interface works.
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2504
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+summary: |
+ The upower-observe interface allows a snap to query UPower for power devices, history
+ and statistics.
+
+ A snap which defines the upower-observe plug must be shown in the interfaces list.
+ The plug must be autoconnected on install and, as usual, must be able to be reconnected.
+
+ The test uses a snap wrapping the upower command line utility, and checks that it can query
+ it without error while the plug is connected.
+
+prepare: |
+ echo "Given a snap declaring a plug on the upower-observe interface is installed"
+ snap install --edge test-snapd-upower-observe-consumer
+
+ apt-get install -y upower
+
+restore: |
+ rm -f upower.error
+ apt-get remove -y upower
+ apt-get autoremove -y
+
+execute: |
+ . "$TESTSLIB/names.sh"
+ CONNECTED_PATTERN=":upower-observe +test-snapd-upower-observe-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-upower-observe-consumer:upower-observe"
+
+ echo "Then it is connected by default"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect test-snapd-upower-observe-consumer:upower-observe ${core_name}:upower-observe
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap is not able to dump info about the upower devices"
+ if su -l -c "test-snapd-upower-observe-consumer.upower --dump 2>${PWD}/upower.error" test; then
+ echo "Expected permission error accessing upower info with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" upower.error
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect test-snapd-upower-observe-consumer:upower-observe ${core_name}:upower-observe
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to dump info about the upower devices"
+ expected="(?s)Device: +/org/freedesktop/UPower/devices/DisplayDevice.*Daemon:.*"
+ # debug
+ su -l -c 'test-snapd-upower-observe-consumer.upower --dump' test || true
+ su -l -c 'test-snapd-upower-observe-consumer.upower --dump' test | grep -Pqz "$expected"
--- /dev/null
+summary: Check snap known --store
+execute: |
+ echo "Check getting assertion from the store"
+ output=$(snap known --remote model series=16 brand-id=canonical model=pi2)
+ echo $output |grep "type: model"
+ echo $output |grep "series: 16"
+ echo $output |grep "brand-id: canonical"
+ echo $output |grep "model: pi2"
--- /dev/null
+summary: Check snap known
+execute: |
+ echo "Listing all account assertions"
+ snap known account|grep "^type: account$"
+ snap known account|grep -c "^account-id: canonical$"
+
+ echo "Finding one account assertion with filters"
+ cnt=$(snap known account account-id=canonical|grep -c "^type: account$")
+ [ "$cnt" -eq 1 ]
+ snap known account|grep -c "^account-id: canonical$"
+ snap known account|grep -c "^username: canonical$"
+
+ echo "Searching non existing assertion"
+ cnt=$(snap known account account-id=non-existing|grep -c "^type: account$" || true)
+ [ "$cnt" -eq 0 ]
--- /dev/null
+summary: Check snap listings
+
+prepare: |
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+execute: |
+ . "$TESTSLIB/names.sh"
+
+ echo "List prints core snap version"
+ if [ "$SPREAD_BACKEND" = "linode" -o "$SPREAD_BACKEND" == "qemu" ] && [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then
+ echo "With customized images the ubuntu-core snap is sideloaded"
+ expected="^${core_name} +.*? +((\d{2}\.\d{2}\.\d+)|\w{12}) + x\d+ +- *"
+ else
+ expected="^${core_name} +.*? +((\d{2}\.\d{2}\.\d+)|\w{12}) + \d+ +canonical +- *"
+ fi
+ snap list | grep -Pq "$expected"
+
+ echo "List prints installed snap version"
+ expected="^test-snapd-tools +(\\d+)(\\.\\d+)* +x[0-9]+ +-"
+ snap list | grep -Pq "$expected"
+
+ echo "Install test-snapd-tools again"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ echo "And run snap list --all"
+ output=$(snap list --all |grep test-snapd-tools)
+ if [ $(printf "$output" | grep test-snapd-tools | wc -l) != "2" ]; then
+ echo "Expected two test-snapd-tools in the output, got:"
+ echo $output
+ exit 1
+ fi
+ if [ $(printf "$output" | grep disabled | wc -l) != "1" ]; then
+ echo "Expected one disabled line in in the output, got:"
+ echo $output
+ exit 1
+ fi
--- /dev/null
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+func main() {
+ sha3_384, _, err := asserts.SnapFileSHA3_384(os.Args[1])
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cannot compute digest: %v\n", err)
+ os.Exit(1)
+ }
+ fmt.Fprintf(os.Stdout, "%s\n", sha3_384)
+}
--- /dev/null
+summary: Checks for local install with metadata from assertions
+# XXX we would need to bother with curl there atm
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+restore: |
+ rm -f test-snapd-tools_*.snap
+execute: |
+ echo "Get the snap"
+ snap download test-snapd-tools
+
+ echo "Try to install the snap without assertions"
+ (snap install test-snapd-tools_*.snap 2>&1 || true) | grep -q 'cannot find signatures with metadata for snap "test-snapd-tools.*\.snap"'
+
+ echo "Get its assertions"
+ # XXX snap download should do this as well
+ curl -H "Accept: application/x.ubuntu.assertion" -o snap-decl.assertion https://assertions.ubuntu.com/v1/assertions/snap-declaration/16/eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw
+ # XXX have a 'snap snap-digest' command?
+ digest=$(go run digest.go test-snapd-tools_*.snap)
+ curl -H "Accept: application/x.ubuntu.assertion" -o snap-rev.assertion https://assertions.ubuntu.com/v1/assertions/snap-revision/${digest}
+ # add them
+ snap ack snap-decl.assertion
+ snap ack snap-rev.assertion
+
+ echo "Installing the snap file will use the metadata from assertions"
+ snap install test-snapd-tools_*.snap
+
+ echo "The revision is not a local revision"
+ snap list|grep ^test-snapd-tools|grep -E " [0-9]+\s+canonical"
+
+ echo "Test it"
+ test-snapd-tools.success
--- /dev/null
+spawn snap login
+
+expect {
+ "Email address:" {
+ exit 0
+ } default {
+ exit 1
+ }
+}
--- /dev/null
+spawn snap login $env(SPREAD_STORE_USER)
+
+expect "Password of "
+send "$env(SPREAD_STORE_PASSWORD)\n"
+
+expect {
+ "Login successful" {
+ exit 0
+ } default {
+ exit 1
+ }
+}
--- /dev/null
+summary: Checks for snap login
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+restore: |
+ snap logout || true
+
+execute: |
+ echo "Checking missing email error"
+ expect -d -f missing_email_error.exp
+
+ echo "Checking wrong password error"
+ expect -d -f unsuccessful_login.exp
+
+ output=$(snap managed)
+ if [ "$output" != "false" ]; then
+ echo "Unexpected output from 'snap managed': $output"
+ exit 1
+ fi
+
+ if [[ $SPREAD_STORE_USER && $SPREAD_STORE_PASSWORD ]]; then
+ echo "Checking successful login"
+ expect -d -f successful_login.exp
+
+ output=$(snap managed)
+ if [ "$output" != "system is managed" ]; then
+ echo "Unexpected output from 'snap managed': $output"
+ exit 1
+ fi
+
+ snap logout
+ fi
+
--- /dev/null
+set timeout 60
+
+spawn snap login someemail@testing.com
+
+expect "Password of "
+send "wrong-password\n"
+
+expect {
+ "not correct" {
+ exit 0
+ } default {
+ exit 1
+ }
+}
--- /dev/null
+summary: Check that all tasks of a failed installtion are undone
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+restore: |
+ rm -rf /snap/test-snapd-tools
+
+execute: |
+ check_empty_glob(){
+ local base_path=$1
+ local glob=$2
+ [ $(find $base_path -maxdepth 1 -name "$glob" | wc -l) -eq 0 ]
+ }
+
+ echo "Given we make a snap uninstallable"
+ mkdir -p /snap/test-snapd-tools/current/foo
+
+ echo "And we try to install it"
+ . $TESTSLIB/snaps.sh
+ if install_local test-snapd-tools; then
+ echo "A snap shouldn't be installable if its mount point is busy"
+ exit 1
+ fi
+
+ echo "Then the snap isn't installed"
+ ! snap list | grep -q test-snapd-tools
+
+ echo "And the installation task is reported as an error"
+ failed_task_id=$(snap changes | perl -ne 'print $1 if /(\d+) +Error.*?Install \"test-snapd-tools\" snap/')
+ if [ -z $failed_task_id ]; then
+ echo "Installation task should be reported as error"
+ exit 1
+ fi
+
+ echo "And the Mount subtask is actually undone"
+ snap change $failed_task_id | grep -Pq "Undone +.*?Mount snap \"test-snapd-tools\""
+ check_empty_glob /snap/test-snapd-tools [0-9]+
+ check_empty_glob /var/lib/snapd/snaps test-snapd-tools_[0-9]+.snap
+
+ echo "And the Data Copy subtask is actually undone"
+ snap change $failed_task_id | grep -Pq "Undone +.*?Copy snap \"test-snapd-tools\" data"
+ check_empty_glob $HOME/snap/test-snapd-tools [0-9]+
+ check_empty_glob /var/snap/test-snapd-tools [0-9]+
+
+ echo "And the Security Profiles Setup subtask is actually undone"
+ snap change $failed_task_id | grep -Pq "Undone +.*?Setup snap \"test-snapd-tools\" \(unset\) security profiles"
+ check_empty_glob /var/lib/snapd/apparmor/profiles snap.test-snapd-tools.*
+ check_empty_glob /var/lib/snapd/seccomp/profiles snap.test-snapd-tools.*
+ check_empty_glob /etc/dbus-1/system.d snap.test-snapd-tools.*.conf
+ check_empty_glob /etc/udev/rules.d 70-snap.test-snapd-tools.*.rules
--- /dev/null
+summary: Check that a remove operation is working even if the mount point is busy.
+
+restore: |
+ kill %1 || true
+
+execute: |
+ wait_for_service(){
+ local service_name=$1
+ local state=$2
+ while ! systemctl show -p ActiveState $service_name | grep -q "ActiveState=$state"; do systemctl status $service_name || true; sleep 1; done
+ }
+ wait_for_remove_state(){
+ local state=$1
+ local expected="(?s)$state.*?Remove \"test-snapd-tools\" snap"
+ while ! snap changes | grep -Pq "$expected"; do sleep 1; done
+ }
+
+ . $TESTSLIB/systemd.sh
+
+ echo "Given a snap is installed"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+ echo "And its mount point is kept busy"
+ # we need a marker file, because just using systemd to figure out
+ # if the service has started is racy, start just means started,
+ # not that the dir is actually blocked yet
+ MARKER=/var/snap/test-snapd-tools/current/block-running
+ rm -f $MARKER
+
+ systemd_create_and_start_unit unmount-blocker "$(which test-snapd-tools.block)"
+
+ wait_for_service unmount-blocker active
+ while [ ! -f $MARKER ]; do sleep 1; done
+
+ echo "When we try to remove the snap"
+ snap remove test-snapd-tools &
+
+ echo "Then the remove retry succeeds"
+ wait_for_remove_state Done
+
+ echo "And the snap is removed"
+ while snap list | grep -q test-snapd-tools; do sleep 1; done
+
+ # cleanup umount blocker
+ systemd_stop_and_destroy_unit unmount-blocker
+
--- /dev/null
+summary: Check snap remove operations.
+
+restore: |
+ rm -f basic_1.0_all.snap
+
+execute: |
+ snap_revisions(){
+ local snap_name=$1
+ echo -n $(find /snap/"$snap_name"/ -maxdepth 1 -type d -name "x*" | wc -l)
+ }
+
+ echo "Given two revisions of a snap have been installed"
+ snapbuild $TESTSLIB/snaps/basic .
+ snap install --dangerous basic_1.0_all.snap
+ snap install --dangerous basic_1.0_all.snap
+
+ echo "Then the two revisions are available on disk"
+ [ $(snap_revisions basic) = "2" ]
+
+ echo "When the snap is removed"
+ snap remove basic
+
+ echo "Then the two revisions are removed from disk"
+ [ $(snap_revisions basic) = "0" ]
+
+ echo "When the snap is removed again, snap exits with status 0"
+ snap remove basic 2> stderr.out
+ cat stderr.out | MATCH 'snap "basic" is not installed'
+
--- /dev/null
+summary: Check that postrm purge works
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+execute: |
+ echo "When some snaps are installed"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ snap install test-snapd-control-consumer
+
+ echo "And snapd is purged"
+ # only available on trusty
+ if [ -x ${SPREAD_PATH}/debian/snapd.prerm ]; then
+ sh -x ${SPREAD_PATH}/debian/snapd.prerm
+ fi
+ sh -x ${SPREAD_PATH}/debian/snapd.postrm purge
+
+ echo "Nothing is left"
+ for d in /snap /var/snap; do
+ if [ -d "$d" ]; then
+ echo "$d is not removed"
+ ls -lR $d
+ exit 1
+ fi
+ done
--- /dev/null
+summary: Check that prepare-image works for grub-systems
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+backends: [-autopkgtest]
+# TODO: use the real stores with proper assertions fully as well once possible
+environment:
+ ROOT: /tmp/root
+ IMAGE: /tmp/root/image
+ GADGET: /tmp/root/gadget
+ STORE_DIR: $(pwd)/fake-store-blobdir
+ STORE_ADDR: localhost:11028
+ UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_SNAPS: 1
+prepare: |
+ . $TESTSLIB/store.sh
+ setup_store fake $STORE_DIR
+restore: |
+ . $TESTSLIB/store.sh
+ teardown_store fake $STORE_DIR
+ rm -rf $ROOT
+execute: |
+ echo Expose the needed assertions through the fakestore
+ cp $TESTSLIB/assertions/developer1.account $STORE_DIR/asserts
+ cp $TESTSLIB/assertions/developer1.account-key $STORE_DIR/asserts
+ # have snap use the fakestore for assertions
+ export SNAPPY_FORCE_SAS_URL=http://$STORE_ADDR
+
+ echo Running prepare-image
+ su -c "snap prepare-image --channel edge --extra-snaps snapweb $TESTSLIB/assertions/developer1-pc.model $ROOT" test
+
+ echo Verifying the result
+ ls -lR $IMAGE
+ for f in pc pc-kernel core snapweb; do
+ ls $IMAGE/var/lib/snapd/seed/snaps/${f}*.snap
+ done
+ grep snap_core=core $IMAGE/boot/grub/grubenv
+ grep snap_kernel=pc-kernel $IMAGE/boot/grub/grubenv
+
+ # check copied assertions
+ cmp $TESTSLIB/assertions/developer1-pc.model $IMAGE/var/lib/snapd/seed/assertions/model
+ cmp $TESTSLIB/assertions/developer1.account $IMAGE/var/lib/snapd/seed/assertions/developer1.account
+
+ echo Verify the unpacked gadget
+ ls -lR $GADGET
+ ls $GADGET/meta/snap.yaml
+
+ echo "Verify that we have valid looking seed.yaml"
+ cat $IMAGE/var/lib/snapd/seed/seed.yaml
+ # snap-id of core
+ grep -q "snap-id: 99T7MUlRhtI3U0QFgl5mXXESAiSwt776" $IMAGE/var/lib/snapd/seed/seed.yaml
+ for snap in pc pc-kernel core; do
+ grep -q "name: $snap" $IMAGE/var/lib/snapd/seed/seed.yaml
+ done
+
+ echo "Verify that we got some snap assertions"
+ for name in pc pc-kernel core; do
+ grep "snap-name: $name" $IMAGE/var/lib/snapd/seed/assertions/*
+ done
--- /dev/null
+summary: Check that prepare-image works for uboot-systems
+environment:
+ ROOT: /tmp/root
+ IMAGE: /tmp/root/image
+ GADGET: /tmp/root/gadget
+prepare: |
+ mkdir -p $ROOT
+ chown test:test $ROOT
+restore:
+ rm -rf $ROOT
+execute: |
+ # TODO: switch to a prebuilt properly signed model assertion once we can do that consistently
+ echo Creating model assertion
+ cat > $ROOT/model.assertion <<EOF
+ type: model
+ series: 16
+ authority-id: my-brand
+ brand-id: my-brand
+ model: my-model
+ architecture: armhf
+ gadget: pi2
+ kernel: pi2-kernel
+ timestamp: 2016-01-02T10:00:00-05:00
+ sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
+
+ AXNpZw==
+ EOF
+
+ echo The unverified model assertion will not be copied into the image
+ export UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL=1
+
+ echo Running prepare-image as a user
+ su -c "snap prepare-image --channel edge --extra-snaps snapweb $ROOT/model.assertion $ROOT" test
+
+ echo Verifying the result
+ ls -lR $IMAGE
+ for f in pi2 pi2-kernel core snapweb; do
+ ls $IMAGE/var/lib/snapd/seed/snaps/${f}*.snap
+ done
+ grep snap_core=core $IMAGE/boot/uboot/uboot.env
+ grep snap_kernel=pi2-kernel $IMAGE/boot/uboot/uboot.env
+
+ echo Verify that the kernel is available unpacked
+ ls $IMAGE/boot/uboot/pi2-kernel_*.snap/kernel.img
+ ls $IMAGE/boot/uboot/pi2-kernel_*.snap/initrd.img
+ ls $IMAGE/boot/uboot/pi2-kernel_*.snap/dtbs/
+
+ echo Verify the unpacked gadget
+ ls -lR $GADGET
+ ls $GADGET/meta/snap.yaml
+
+ echo Verify that we have valid looking seed.yaml
+ cat $IMAGE/var/lib/snapd/seed/seed.yaml
+ # snap-id of core
+ grep -q "snap-id: 99T7MUlRhtI3U0QFgl5mXXESAiSwt776" $IMAGE/var/lib/snapd/seed/seed.yaml
+ for snap in pi2 pi2-kernel core; do
+ grep -q "name: $snap" $IMAGE/var/lib/snapd/seed/seed.yaml
+ done
+
+ echo "Verify that we got some snap assertions"
+ for name in pi2 pi2-kernel core; do
+ grep "snap-name: $name" $IMAGE/var/lib/snapd/seed/assertions/*
+ done
--- /dev/null
+summary: Check that undo for snap refresh works
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+environment:
+ BLOB_DIR: $(pwd)/fake-store-blobdir
+ GOOD_SNAP: test-snapd-python-webserver
+ BAD_SNAP: test-snapd-tools
+
+prepare: |
+ . $TESTSLIB/store.sh
+
+ echo "Given two snaps are installed"
+ for snap in $GOOD_SNAP $BAD_SNAP; do
+ snap install $snap
+ done
+
+ echo "And the daemon is configured to point to the fake store"
+ setup_store fake $BLOB_DIR
+
+restore: |
+ . $TESTSLIB/store.sh
+ teardown_store fake $BLOB_DIR
+
+execute: |
+ echo "When the store is configured to make them refreshable"
+ fakestore -make-refreshable $GOOD_SNAP,$BAD_SNAP -dir $BLOB_DIR
+
+ echo "When a snap is broken"
+ echo "i-am-broken-now" >> $BLOB_DIR/${BAD_SNAP}*fake1*.snap
+
+ echo "And a refresh is performed"
+ if snap refresh ; then
+ echo "snap refresh should fail but it did not, test is broken"
+ exit 1
+ fi
+
+ echo "Then the new version of the good snap got installed"
+ snap list | MATCH -E "${GOOD_SNAP}.*?fake1"
+
+ echo "But the bad snap did not get updated"
+ snap list | MATCH -E "${BAD_SNAP}"| MATCH -v "fake"
+
+ echo "Verify the snap change"
+ snap change 4 | MATCH "Undone.*Download snap \"${BAD_SNAP}\""
+ snap change 4 | MATCH "Done.*Download snap \"${GOOD_SNAP}\""
+ snap change 4 | MATCH "ERROR cannot verify snap \"test-snapd-tools\", no matching signatures found"
--- /dev/null
+summary: Check that more than one snap is refreshed.
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+details: |
+ We use only the fake store for this test because we currently
+ have only one controlled snap in the remote stores, when we will
+ have more we can update the test to use them
+
+environment:
+ BLOB_DIR: $(pwd)/fake-store-blobdir
+
+prepare: |
+ . $TESTSLIB/store.sh
+
+ echo "Given two snaps are installed"
+ for snap in test-snapd-tools test-snapd-python-webserver; do
+ snap install $snap
+ done
+
+ echo "And the daemon is configured to point to the fake store"
+ setup_store fake $BLOB_DIR
+
+restore: |
+ . $TESTSLIB/store.sh
+ teardown_store fake $BLOB_DIR
+
+execute: |
+ echo "When the store is configured to make them refreshable"
+ fakestore -make-refreshable test-snapd-tools,test-snapd-python-webserver -dir $BLOB_DIR
+
+ echo "And a refresh is performed"
+ snap refresh
+
+ echo "Then the new versions are installed"
+ for snap in test-snapd-tools test-snapd-python-webserver; do
+ snap list | grep -Pq "$snap.*?fake1"
+ done
--- /dev/null
+summary: Check that the refresh command works.
+details: |
+ These tests exercise the refresh command using different store backends.
+ The concrete store to be used is controlled with the STORE_TYPE environment
+ vairiable, if it is empty, a fake local store is used, if it has a value of
+ staginig or production the corresponding remote store is used.
+ When executing against the remote stores the tests rely in the existence of
+ a given snap with an updatable version (version string like 2.0+fake1) in the
+ edge channel.
+
+environment:
+ SNAP_NAME: test-snapd-tools
+ SNAP_VERSION_PATTERN: \d+\.\d+\+fake1
+ BLOB_DIR: $(pwd)/fake-store-blobdir
+ STORE_TYPE/fake: fake
+# STORE_TYPE/staging: staging
+ STORE_TYPE/production: production
+
+prepare: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "Given a snap is installed"
+ snap install --devmode test-snapd-tools
+ fi
+
+ . $TESTSLIB/store.sh
+ setup_store $STORE_TYPE $BLOB_DIR
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "And a new version of that snap put in the controlled store"
+ fakestore -dir $BLOB_DIR -make-refreshable test-snapd-tools
+ fi
+
+restore: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ . $TESTSLIB/store.sh
+ teardown_store $STORE_TYPE $BLOB_DIR
+
+execute: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ # FIXME: currently the --list from channel doesn't work
+ # echo "Then the new version is available for the snap to be refreshed"
+ # expected="$SNAP_NAME +$SNAP_VERSION_PATTERN"
+ # snap refresh --list | grep -Pzq "$expected"
+ #
+ # echo "================================="
+
+ echo "When the snap is refreshed"
+ snap refresh --devmode --channel=edge $SNAP_NAME
+
+ echo "Then the new version is listed"
+ expected="$SNAP_NAME +$SNAP_VERSION_PATTERN .*devmode"
+ snap list | grep -Pzq "$expected"
--- /dev/null
+summary: Check that the undo on refresh keeps the previous snap intact
+details: |
+ When a snap is refreshed and the refresh fails, the undo code had
+ a bug that removed the security confinement (LP: #1637981)
+
+environment:
+ SNAP_NAME: test-snapd-service
+ SNAP_NAME_GOOD: ${SNAP_NAME}-v1-good
+ SNAP_NAME_BAD: ${SNAP_NAME}-v2-bad
+ SNAP_FILE_GOOD: ${SNAP_NAME}_1.0_all.snap
+ SNAP_FILE_BAD: ${SNAP_NAME}_2.0_all.snap
+
+prepare: |
+ echo "Given a good (v1) and a bad (v2) snap"
+ snapbuild $TESTSLIB/snaps/$SNAP_NAME_GOOD .
+ snapbuild $TESTSLIB/snaps/$SNAP_NAME_BAD .
+
+debug: |
+ journalctl -u snap.test-snapd-service.service.service
+
+execute: |
+ wait_for_service_status() {
+ retries=0
+ while ! systemctl status snap.test-snapd-service.service.service|grep "$1"; do
+ # retry
+ retries=$((retries+1))
+ if [ $retries -gt 20 ]; then
+ echo 'expected "service v1" output did not appear in systemctl status snap.test-snapd-service.service.service'
+ exit 1
+ fi
+ sleep 1
+ done
+ }
+ echo "When we install v1"
+ snap install --dangerous ${SNAP_FILE_GOOD}
+ echo "The v1 service started correctly"
+ wait_for_service_status "service v1"
+
+ echo "When we refresh to v2"
+ if snap install --dangerous ${SNAP_FILE_BAD}; then
+ echo "The ${SNAP_FILE_BAD} snap should not install cleanly, test broken"
+ exit 1
+ fi
+ echo "Then v2 is rolled back and v1 is started again"
+ wait_for_service_status "service v1"
--- /dev/null
+summary: Check that the refresh command works.
+details: |
+ These tests exercise the refresh command using different store backends.
+ The concrete store to be used is controlled with the STORE_TYPE environment
+ vairiable, if it is empty, a fake local store is used, if it has a value of
+ staginig or production the corresponding remote store is used.
+ When executing against the remote stores the tests rely in the existence of
+ a given snap with an updatable version (version string like 2.0+fake1) in the
+ edge channel.
+
+environment:
+ SNAP_NAME: test-snapd-tools
+ SNAP_VERSION_PATTERN: \d+\.\d+\+fake1
+ BLOB_DIR: $(pwd)/fake-store-blobdir
+ STORE_TYPE/fake: fake
+# STORE_TYPE/staging: staging
+ STORE_TYPE/production: production
+
+prepare: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "Given a snap is installed"
+ snap install test-snapd-tools
+ fi
+
+ . $TESTSLIB/store.sh
+ setup_store $STORE_TYPE $BLOB_DIR
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "And a new version of that snap put in the controlled store"
+ fakestore -dir $BLOB_DIR -make-refreshable test-snapd-tools
+ fi
+
+restore: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ . $TESTSLIB/store.sh
+ teardown_store $STORE_TYPE $BLOB_DIR
+
+execute: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ # FIXME: currently the --list from channel doesn't work
+ # echo "Then the new version is available for the snap to be refreshed"
+ # expected="$SNAP_NAME +$SNAP_VERSION_PATTERN"
+ # snap refresh --list | grep -Pzq "$expected"
+ #
+ # echo "================================="
+
+ echo "When the snap is refreshed"
+ snap refresh --channel=edge $SNAP_NAME
+
+ echo "Then the new version is listed"
+ expected="$SNAP_NAME +$SNAP_VERSION_PATTERN"
+ snap list | grep -Pzq "$expected"
+
+ echo "When a snap is refreshed and has no update it exit 0"
+ snap refresh $SNAP_NAME 2>stderr.out
+ cat stderr.out | MATCH "snap \"$SNAP_NAME\" has no updates available"
\ No newline at end of file
--- /dev/null
+summary: Regression test that ensures that $HOME/snap is not root owned for sudo commands
+
+prepare: |
+ # ensure we have no snap user data directory yet
+ rm -rf /home/test/snap
+ rm -rf /root/snap
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+execute: |
+ # run a snap command via sudo
+ output=$(su -l -c "sudo /snap/bin/test-snapd-tools.env" test)
+
+ # ensure SNAP_USER_DATA points to the right place
+ echo $output | grep -E SNAP_USER_DATA=/root/snap/test-snapd-tools/x[0-9]+
+ echo $output | grep -E HOME=/root/snap/test-snapd-tools/x[0-9]+
+ echo $output | grep SNAP_USER_COMMON=/root/snap/test-snapd-tools/common
+
+ echo "Verify that the /root/snap directory created and root owned"
+ if [ $(stat -c '%U' /root/snap) != "root" ]; then
+ echo "The /root/snap directory is not owned by root"
+ ls -ld /snap/snap
+ exit 1
+ fi
+
+ echo "Verify that there is no /home/test/snap appearing"
+ if [ -e /home/test/snap ]; then
+ user=$(stat -c '%U' /home/test/snap)
+ echo "An unexpected /home/test/snap directory got created (owner $user)"
+ ls -ld /home/test/snap
+ exit 1
+ fi
--- /dev/null
+summary: snaps installed with --jailmode are not in devmode
+details: |
+ Users found that a snap that uses "confinement: devmode", even when
+ installed with "snap install --jailmode" is effectively in devmode (the
+ apparmor profile is in complain mode) even if "snap list" reports it as
+ "jailmode".
+ This has been reported as https://bugs.launchpad.net/snappy/+bug/1641885
+prepare: |
+ snapbuild $TESTSLIB/snaps/test-snapd-devmode .
+ echo "Install a test snap that uses 'confinement: devmode' in jailmode"
+ snap install --jailmode --dangerous ./test-snapd-devmode_1.0_all.snap
+execute: |
+ echo "Ensure that the snap is installed in jailmode"
+ snap list | grep test-snapd-devmode | grep jailmode
+ echo "Ensure that the apparmor profile doesn't use the complain mode"
+ cat /var/lib/snapd/apparmor/profiles/snap.test-snapd-devmode.test-snapd-devmode | grep attach_disconnected | grep -v complain
+ echo "Ensure that the seccomp profile doesn't use the complain mode"
+ cat /var/lib/snapd/apparmor/profiles/snap.test-snapd-devmode.test-snapd-devmode | grep -v '@complain'
+restore: |
+ rm -f ./test-snapd-devmode_1.0_all.snap
+debug: |
+ echo "Apparmor profile (first 30 lines)"
+ head -n 30 /var/lib/snapd/apparmor/profiles/snap.test-snapd-devmode.test-snapd-devmode || true
+ echo "Seccomp profile (first 30 lines)"
+ head -n 30 /var/lib/snapd/seccomp/profiles/snap.test-snapd-devmode.test-snapd-devmode || true
--- /dev/null
+summary: Check remove command errors
+
+execute: |
+ echo "Given a core snap is installed"
+ . "$TESTSLIB/snaps.sh"
+ install_local test-snapd-tools
+
+ . "$TESTSLIB/names.sh"
+ echo "Ensure the important snaps can not be removed"
+ for sn in $core_name $kernel_name $gadget_name; do
+ if snap remove $sn; then
+ echo "It should not be possible to remove $sn"
+ exit 1
+ fi
+ done
--- /dev/null
+summary: Check that revert of a snap in devmode restores devmode
+environment:
+ STORE_TYPE/fake: fake
+# STORE_TYPE/staging: staging
+ STORE_TYPE/production: production
+ BLOB_DIR: $(pwd)/fake-store-blobdir
+
+prepare: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "Given a snap is installed"
+ snap install --devmode test-snapd-tools
+ fi
+
+ . $TESTSLIB/store.sh
+ setup_store $STORE_TYPE $BLOB_DIR
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "And a new version of that snap put in the controlled store"
+ fakestore -dir $BLOB_DIR -make-refreshable test-snapd-tools
+ fi
+
+restore: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ . $TESTSLIB/store.sh
+ teardown_store $STORE_TYPE $BLOB_DIR
+
+execute: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ echo "When a refresh is made"
+ snap refresh --devmode --edge test-snapd-tools
+
+ echo "Then the new version is installed"
+ snap list | grep -Pq "test-snapd-tools +\d+\.\d+\+fake1"
+ LATEST=$(readlink /snap/test-snapd-tools/current)
+
+ echo "When a revert is made without --devmode flag"
+ snap revert test-snapd-tools
+
+ echo "Then the old version is active"
+ snap list | grep -Pq "test-snapd-tools +\d+\.\d+ "
+
+ echo "And the snap runs confined"
+ snap list|grep test-snapd-tools|grep -q "-"
+
+ echo "When the latest revision is installed again"
+ snap remove --revision=$LATEST test-snapd-tools
+ snap refresh --devmode --edge test-snapd-tools
+
+ echo "And revert is made with --devmode flag"
+ snap revert --devmode test-snapd-tools
+
+ echo "Then snap uses devmode"
+ snap list|grep test-snapd-tools|grep -q devmode
--- /dev/null
+summary: Checks for snap sideload reverts
+
+prepare: |
+ snapbuild $TESTSLIB/snaps/basic .
+
+restore: |
+ rm ./basic_1.0_all.snap
+
+execute: |
+ echo Installing sideloaded snap
+ snap install --dangerous ./basic_1.0_all.snap
+ snap list | grep -Pzq "x1"
+
+ echo Installing new version of sideloaded snap
+ snap install --dangerous ./basic_1.0_all.snap
+ snap list | grep -Pzq "x2"
+
+ echo Reverting to the previous version
+ snap revert basic | grep reverted
+ snap list | grep -Pzq "x1"
--- /dev/null
+summary: Check that revert works.
+
+environment:
+ STORE_TYPE/fake: fake
+# STORE_TYPE/staging: staging
+ STORE_TYPE/production: production
+ BLOB_DIR: $(pwd)/fake-store-blobdir
+
+prepare: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "Given a snap is installed"
+ snap install test-snapd-tools
+ fi
+
+ . $TESTSLIB/store.sh
+ setup_store $STORE_TYPE $BLOB_DIR
+
+ if [ "$STORE_TYPE" = "fake" ]; then
+ echo "And a new version of that snap put in the controlled store"
+ fakestore -dir $BLOB_DIR -make-refreshable test-snapd-tools
+ fi
+
+restore: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ . $TESTSLIB/store.sh
+ teardown_store $STORE_TYPE $BLOB_DIR
+
+execute: |
+ if [[ "$STORE_TYPE" = "fake" ]] && [[ "$SPREAD_SYSTEM" =~ ubuntu-core-16-* ]]; then
+ exit
+ fi
+
+ echo "When a refresh is made"
+ snap refresh --edge test-snapd-tools
+
+ echo "Then the new version is installed"
+ snap list | grep -Pq "test-snapd-tools +\d+\.\d+\+fake1"
+
+ echo "When a revert is made"
+ snap revert test-snapd-tools
+
+ echo "Then the old version is active"
+ snap list | grep -Pq "test-snapd-tools +\d+\.\d+ "
+
+ echo "And the data directories are present"
+ ls /snap/test-snapd-tools | grep -q current
+ ls /var/snap/test-snapd-tools | grep -q current
+
+ echo "And the snap runs confined"
+ snap list|grep test-snapd-tools|grep -q "-"
+
+ echo "And a new revert fails"
+ if snap revert test-snapd-tools; then
+ echo "A revert on an already reverted snap should fail"
+ exit 1
+ fi
+
+ echo "And a refresh doesn't update the snap"
+ snap refresh
+ snap list | grep -Pq "test-snapd-tools +\d+\.\d+ "
+
+ echo "Unless the snap is asked for explicitly"
+ snap refresh --edge test-snapd-tools
+ snap list | grep -Pq "test-snapd-tools +\d+\.\d+\+fake1"
--- /dev/null
+summary: Check snap search
+
+execute: |
+ echo "List all featured snaps"
+ expected="(?s)Name +Version +Developer +Notes +Summary *\n\
+ (.*?\n)?\
+ .*"
+ snap find | grep -Pzq "$expected"
+ if [ $(snap find | wc -l) -gt 50 ]; then
+ echo "Found more than 50 featured apps, this seems bogus:"
+ snap find
+ exit 1
+ fi
+ if [ $(snap find | wc -l) -lt 2 ]; then
+ echo "Not found any featured app, this seems bogus:"
+ snap find
+ exit 1
+ fi
+
+ echo "Exact matches"
+ for snapName in test-snapd-tools test-snapd-python-webserver
+ do
+ expected="(?s)Name +Version +Developer +Notes +Summary *\n\
+ (.*?\n)?\
+ $snapName +.*? *\n\
+ .*"
+ snap find $snapName | grep -Pzq "$expected"
+ done
+
+ echo "Partial terms work too"
+ expected="(?s)Name +Version +Developer +Notes +Summary *\n\
+ (.*?\n)?\
+ test-snapd-tools +.*? *\n\
+ .*"
+ snap find test-snapd- | grep -Pzq "$expected"
+
+ # cassandra only available for amd64
+ if [ $(uname -m) = "x86_64" ]; then
+ echo "List of snaps in a section works"
+ snap find --section=database | MATCH cassandra
+ fi
--- /dev/null
+summary: Check basic apparmor confinement rules.
+
+prepare: |
+ echo "Given a basic snap is installed"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+execute: |
+ echo "Then an unconfined action should succeed"
+ test-snapd-tools.cmd touch /dev/shm/snap.test-snapd-tools.foo
+ test -f /dev/shm/snap.test-snapd-tools.foo
+
+ echo "Then a confined action should fail"
+ if test-snapd-tools.cmd touch /dev/shm/snap.not-test-snapd-tools.foo 2>touch.error; then
+ echo "Expected error"
+ exit 1
+ fi
+ [ "$(cat touch.error)" = "touch: cannot touch '/dev/shm/snap.not-test-snapd-tools.foo': Permission denied" ]
--- /dev/null
+summary: Ensure that the security rules related to device cgroups work.
+
+environment:
+ DEVICE_NAME/kmsg: kmsg
+ UDEVADM_PATH/kmsg: /sys/devices/virtual/mem/kmsg
+ DEVICE_ID/kmsg: "c 1:11 rwm"
+ OTHER_DEVICE_NAME/kmsg: uinput
+ OTHER_UDEVADM_PATH/kmsg: /sys/devices/virtual/misc/uinput
+ OTHER_DEVICE_ID/kmsg: "c 10:223 rwm"
+
+ DEVICE_NAME/uinput: uinput
+ UDEVADM_PATH/uinput: /sys/devices/virtual/misc/uinput
+ DEVICE_ID/uinput: "c 10:223 rwm"
+ OTHER_DEVICE_NAME/uinput: kmsg
+ OTHER_UDEVADM_PATH/uinput: /sys/devices/virtual/mem/kmsg
+ OTHER_DEVICE_ID/uinput: "c 1:11 rwm"
+
+restore: |
+ rm -f /etc/udev/rules.d/70-snap.test-snapd-tools.rules
+ udevadm control --reload-rules
+ udevadm trigger
+
+execute: |
+ echo "Given a snap is installed"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+ echo "Then the device is not assigned to that snap"
+ ! udevadm info $UDEVADM_PATH | grep -Pq "E: TAGS=.*?snap_test-snapd-tools_env"
+
+ echo "And the device is not shown in the snap device list"
+ ! grep -q "$DEVICE_ID" /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list
+
+ echo "================================================="
+
+ echo "When a udev rule assigning the device to the snap is added"
+ content="KERNEL==\"$DEVICE_NAME\", TAG+=\"snap_test-snapd-tools_env\""
+ echo "$content" > /etc/udev/rules.d/70-snap.test-snapd-tools.rules
+ udevadm control --reload-rules
+ udevadm settle
+ udevadm trigger
+ udevadm settle
+
+ echo "Then the device is shown as assigned to the snap"
+ udevadm info $UDEVADM_PATH | grep -Pq "E: TAGS=.*?snap_test-snapd-tools_env"
+
+ echo "And other devices are not shown as assigned to the snap"
+ ! udevadm info $OTHER_UDEVADM_PATH | grep -Pq "E: TAGS=.*?snap_test-snapd-tools_env"
+
+ echo "================================================="
+
+ echo "When a snap command is called"
+ test-snapd-tools.env
+
+ echo "Then the device is shown in the snap device list"
+ grep -q "$DEVICE_ID" /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list
+
+ echo "And other devices are not shown in the snap device list"
+ ! grep -q "$OTHER_DEVICE_ID" /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list
+
+ # TODO: check device unassociated after removing the udev file and rebooting
--- /dev/null
+#!/usr/bin/expect -f
+
+set timeout 20
+
+spawn bash
+send "ls /dev/pts\n"
+expect "\[0-9]*ptmx" {} timeout { exit 1 }
+
+# Launch app and checks contents of /dev/pts, should have only ptmx
+spawn su -l -c "/snap/bin/test-snapd-tools.sh" test
+expect "bash-4.3\\$" {} timeout { exit 1 }
+send "ls /dev/pts\n"
+expect {
+ timeout { exit 1 }
+ "\[0-9]" { exit 1 }
+ "ptmx"
+}
+
+# From within confined app, open a pty
+send "python3\n"
+expect ">>>" {} timeout { exit 1 }
+send "import os, pty, sys\n"
+send "os.listdir('/dev/pts')\n"
+expect "\['ptmx'\]" {} timeout { exit 1 }
+send "pty.openpty()\n"
+send "os.listdir('/dev/pts')\n"
+expect "\['0', 'ptmx'\]" {} timeout { exit 1 }
+send "sys.exit(0)\n"
+expect "bash-4.3\\$" {} timeout { exit 1 }
+
+# From with confined app, verify pty was closed (assumes that the last program
+# closes it properly, which the above does)
+send "ls /dev/pts\n"
+expect {
+ timeout { exit 1 }
+ "\[0-9]" { exit 1 }
+ "ptmx"
+}
--- /dev/null
+summary: Ensure that the basic devpts security rules are in place.
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+prepare: |
+ echo "Given a basic snap is installed"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+execute: |
+ echo "Then the pts device follows confinement rules"
+ expect -d -f pts.exp
--- /dev/null
+summary: Ensure that the security rules for private tmp are in place.
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+environment:
+ SNAP_INSTALL_DIR: $(pwd)/snap-install-dir
+
+prepare: |
+ echo "Given a basic snap is installed"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+ echo "And another basic snap is installed"
+ mkdir -p $SNAP_INSTALL_DIR
+ cp -ra $TESTSLIB/snaps/test-snapd-tools/* $SNAP_INSTALL_DIR
+ sed -i 's/test-snapd-tools/not-test-snapd-tools/g' $SNAP_INSTALL_DIR/meta/snap.yaml
+ snapbuild $SNAP_INSTALL_DIR .
+ snap install --dangerous not-test-snapd-tools_1.0_all.snap
+
+restore: |
+ rm -rf not-test-snapd-tools_1.0_all.snap \
+ $SNAP_INSTALL_DIR /tmp/foo *stat.error
+
+execute: |
+ echo "When a temporary file is created by one snap"
+ expect -d -f tmp-create.exp
+
+ if [ -e /usr/lib/snapd/snap-discard-ns ]; then
+ echo "Then that file is accessible from other calls of commands from the same snap"
+ if ! test-snapd-tools.cmd stat /tmp/foo 2>same-stat.error; then
+ echo "Expected the file to be present"
+ exit 1
+ fi
+ else
+ echo "Then that file is not accessible from other calls of commands from the same snap"
+ if test-snapd-tools.cmd stat /tmp/foo 2>same-stat.error; then
+ echo "Expected the file to be absent"
+ exit 1
+ fi
+ fi
+
+ echo "And that file is not accessible by other snaps"
+ if not-test-snapd-tools.cmd stat /tmp/foo 2>other-stat.error; then
+ echo "Expected error not present"
+ exit 1
+ fi
+ grep -q "stat: cannot stat '/tmp/foo': No such file or directory" other-stat.error
--- /dev/null
+#!/usr/bin/expect -f
+
+set timeout 20
+
+spawn bash
+
+# Test private /tmp, allowed access
+spawn su -l -c "/snap/bin/test-snapd-tools.sh" test
+expect "bash-4.3\\$" {} timeout { exit 1 }
+send "touch /tmp/foo\n"
+send "stat /tmp/foo\n"
+expect {
+ timeout { exit 1 }
+ "File: '/tmp/foo'*Size: 0"
+}
--- /dev/null
+summary: Check security profile generation for apps and hooks.
+
+prepare: |
+ snapbuild $TESTSLIB/snaps/basic-hooks .
+restore: |
+ rm basic-hooks_1.0_all.snap
+
+execute: |
+ seccomp_profile_directory="/var/lib/snapd/seccomp/profiles"
+
+ echo "Security profiles are generated and loaded for apps"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles)
+
+ for profile in snap.test-snapd-tools.block snap.test-snapd-tools.cat snap.test-snapd-tools.echo snap.test-snapd-tools.fail snap.test-snapd-tools.success
+ do
+ echo "$loaded_profiles" | grep -zq "$profile (enforce)"
+ [ -f "$seccomp_profile_directory/$profile" ]
+ done
+
+ echo "Security profiles are generated and loaded for hooks"
+ snap install --dangerous basic-hooks_1.0_all.snap
+ loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles)
+
+ echo "$loaded_profiles" | grep -zq "snap.basic-hooks.hook.configure (enforce)"
+ [ -f "$seccomp_profile_directory/snap.basic-hooks.hook.configure" ]
--- /dev/null
+summary: Check snap web servers
+environment:
+ SNAP_NAME/pythonServer: test-snapd-python-webserver
+ IP_VERSION/pythonServer: 4
+ PORT/pythonServer: 80
+ TEXT/pythonServer: XKCD rocks!
+ LOCALHOST/pythonServer: localhost
+ SNAP_NAME/goServer: test-snapd-go-webserver
+ IP_VERSION/goServer: 6
+ PORT/goServer: 8081
+ TEXT/goServer: Hello World
+ LOCALHOST/goServer: ip6-localhost
+
+warn-timeout: 3m
+
+prepare: |
+ snap install $SNAP_NAME
+ cat > request.txt <<EOF
+ GET / HTTP/1.0
+
+ EOF
+ echo "Wait for the service to be listening, limited to the task kill-timeout"
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+restore: |
+ rm -f request.txt
+
+execute: |
+ response=$(nc -q 5 -"$IP_VERSION" "$LOCALHOST" "$PORT" < request.txt)
+
+ statusPattern="(?s)HTTP\/1\.0 200 OK\n*"
+ echo "$response" | grep -Pzq "$statusPattern"
+ echo "$response" | grep -Pzq "$TEXT"
--- /dev/null
+summary: Check that `snap auto-import` works as expected
+
+systems: [ubuntu-core-16-64]
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Ensure the testrootorg-store.account-key is not already added"
+ output=$(snap known account-key | grep "name: test-store"|wc -l)
+ if [ "$output" != "0" ]; then
+ echo " testrootorg-store.account-key is already added"
+ exit 1
+ fi
+ echo "Create a ramdisk with the testrootorg-store.account-key assertion"
+ mkfs.vfat /dev/ram0
+ mount /dev/ram0 /mnt
+ cp $TESTSLIB/assertions/testrootorg-store.account-key /mnt/auto-import.assert
+
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ rm -rf /var/lib/snapd/auto-import/*
+
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Simulate a not running snapd (happens on e.g. early boot)"
+ systemctl stop snapd.service snapd.socket
+
+ echo "`snap auto-import` spooled assertions if it can not talk to snapd"
+ snap auto-import
+ ls /run/snapd/auto-import
+ umount /mnt
+ systemctl start snapd.service snapd.socket
+
+ echo "`snap auto-import` reads from the auto-import dir"
+ snap auto-import
+ snap known account-key | grep "name: test-store"
+
+ nr=$(ls /run/snapd/auto-import|wc -l)
+ if [ "$nr" != "0" ]; then
+ echo "Expected an empty /run/snapd/auto-import got:"
+ ls /run/snapd/auto-import
+ exit 1
+ fi
\ No newline at end of file
--- /dev/null
+summary: Check that `snap auto-import` works as expected
+
+systems: [ubuntu-core-16-64]
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Ensure the testrootorg-store.account-key is not already added"
+ output=$(snap known account-key | grep "name: test-store"|wc -l)
+ if [ "$output" != "0" ]; then
+ echo " testrootorg-store.account-key is already added"
+ exit 1
+ fi
+ echo "Create a ramdisk with the testrootorg-store.account-key assertion"
+ mkfs.vfat /dev/ram0
+ mount /dev/ram0 /mnt
+ cp $TESTSLIB/assertions/testrootorg-store.account-key /mnt/auto-import.assert
+
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ umount /mnt
+
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "`snap auto-import` imports assertions from the mounted ramdisk"
+ snap auto-import
+ snap known account-key | grep "name: test-store"
--- /dev/null
+summary: Check that `snap auto-import` works as expected
+
+systems: [ubuntu-core-16-64]
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Install dmsetup"
+ snap install --devmode --edge dmsetup
+
+ echo "Ensure the testrootorg-store.account-key is not already added"
+ output=$(snap known account-key | grep "name: test-store"|wc -l)
+ if [ "$output" != "0" ]; then
+ echo " testrootorg-store.account-key is already added"
+ exit 1
+ fi
+ echo "Create a ramdisk with the testrootorg-store.account-key assertion"
+ mkfs.vfat /dev/ram0
+ mount /dev/ram0 /mnt
+ cp $TESTSLIB/assertions/testrootorg-store.account-key /mnt/auto-import.assert
+ umount /mnt
+
+ echo "Create new block device to trigger auto-import mount"
+ dmsetup -v --noudevsync --noudevrules create dm-ram0 --table "0 $(blockdev --getsize /dev/ram0) linear /dev/ram0 0"
+
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ dmsetup -v --noudevsync --noudevrules remove dm-ram0
+
+debug: |
+ tail -n 20 /var/log/syslog
+
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "The auto-mount magic has given us the assertion"
+ snap known account-key | grep "name: test-store"
--- /dev/null
+summary: Check that snap connect works
+
+prepare: |
+ . $TESTSLIB/names.sh
+ . $TESTSLIB/snaps.sh
+
+ echo "Install a test snap"
+ install_local home-consumer
+ # the home interface is not autoconnected on all-snap systems
+ if [[ ! "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then
+ snap disconnect home-consumer:home "${core_name}:home"
+ fi
+
+execute: |
+ . $TESTSLIB/names.sh
+
+ CONNECTED_PATTERN=':home +home-consumer'
+
+ echo "The plug can be connected to a matching slot of OS snap without snap:slot argument"
+ snap connect home-consumer:home
+ snap interfaces | MATCH "$CONNECTED_PATTERN"
+
+ snap disconnect home-consumer:home "${core_name}:home"
+
+ echo "The plug can be connected to a matching slot with slot name omitted"
+ snap connect home-consumer:home "${core_name}"
+ snap interfaces | MATCH "$CONNECTED_PATTERN"
+
+ snap disconnect home-consumer:home "${core_name}:home"
+
+ echo "The plug can be connected to a slot on the core snap using abbreviated syntax"
+ snap connect home-consumer:home :home
+ snap interfaces | MATCH "$CONNECTED_PATTERN"
--- /dev/null
+summary: Check that snap disconnect works
+
+systems: [-ubuntu-core-16-64]
+
+environment:
+ SNAP_FILE: "home-consumer_1.0_all.snap"
+
+prepare: |
+ echo "Install a test snap"
+ snapbuild $TESTSLIB/snaps/home-consumer .
+ snap install --dangerous $SNAP_FILE
+
+restore: |
+ rm -f *.snap
+
+execute: |
+ . $TESTSLIB/names.sh
+
+ DISCONNECTED_PATTERN="\-\s+home-consumer:home"
+
+ echo "Disconnect everything from given slot"
+ snap connect home-consumer:home ${core_name}:home
+ snap disconnect ${core_name}:home
+ snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN"
+
+ echo "Disconnect everything from given slot (abbreviated)"
+ snap connect home-consumer:home ${core_name}:home
+ snap disconnect :home
+ snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN"
+
+ echo "Disconnect everything from given plug"
+ snap connect home-consumer:home ${core_name}:home
+ snap disconnect home-consumer:home
+ snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN"
+
+ echo "Disconnect specific plug and slot"
+ snap connect home-consumer:home ${core_name}:home
+ snap disconnect home-consumer:home ${core_name}:home
+ snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN"
+
+ echo "Disconnect specific plug and slot (abbreviated)"
+ snap connect home-consumer:home ${core_name}:home
+ snap disconnect home-consumer:home :home
+ snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN"
--- /dev/null
+summary: Check that snap download works
+restore: |
+ rm -f *.snap
+execute: |
+ verify_asserts() {
+ fn="$1"
+ grep "type: account-key" "$fn"
+ grep "type: snap-declaration" "$fn"
+ grep "type: snap-revision" "$fn"
+ }
+ echo "Snap download can download snaps"
+ snap download test-snapd-control-consumer
+ ls test-snapd-control-consumer_*.snap
+ verify_asserts test-snapd-control-consumer_*.assert
+
+ echo "Snap download understand --edge"
+ snap download --edge test-snapd-tools
+ ls test-snapd-tools_*.snap
+ verify_asserts test-snapd-tools_*.assert
+
+ echo "Snap download downloads devmode snaps"
+ snap download --beta classic
+ ls classic_*.snap
+ verify_asserts classic_*.assert
+
+ echo "Snap download can download snaps as user"
+ su -l -c "snap download test-snapd-tools" test
+ ls /home/test/test-snapd-tools_*.snap
+ verify_asserts /home/test/test-snapd-tools_*.assert
--- /dev/null
+summary: inspect all the set environment variables prefixed with SNAP_ and XDG_
+prepare: |
+ snapbuild $TESTSLIB/snaps/test-snapd-tools .
+ snap install --dangerous test-snapd-tools_1.0_all.snap
+restore: |
+ rm -f *.snap
+execute: |
+ echo "Collect SNAP and XDG environment variables"
+ test-snapd-tools.env | egrep '^SNAP_' | sort > snap-vars.txt
+ test-snapd-tools.env | egrep '^XDG_' | sort > xdg-vars.txt
+
+ echo "Ensure that SNAP environment variables are what we expect"
+ egrep -q '^SNAP_ARCH=(amd64|i386|arm64|armhf|ppc64el)$' snap-vars.txt
+ egrep -q '^SNAP_COMMON=/var/snap/test-snapd-tools/common$' snap-vars.txt
+ egrep -q '^SNAP_DATA=/var/snap/test-snapd-tools/x1$' snap-vars.txt
+ egrep -q '^SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:$' snap-vars.txt
+ egrep -q '^SNAP_NAME=test-snapd-tools$' snap-vars.txt
+ # XXX: probably not something we ought to test
+ # egrep -q '^SNAP_REEXEC=0$' snap-vars.txt
+ egrep -q '^SNAP_REVISION=x1$' snap-vars.txt
+ egrep -q '^SNAP_USER_COMMON=/root/snap/test-snapd-tools/common$' snap-vars.txt
+ egrep -q '^SNAP_USER_DATA=/root/snap/test-snapd-tools/x1$' snap-vars.txt
+ egrep -q '^SNAP_VERSION=1.0$' snap-vars.txt
+ test $(wc -l < snap-vars.txt) -eq 10
+
+ echo "Enure that XDG environment variables are what we expect"
+ egrep -q '^XDG_RUNTIME_DIR=/run/user/0/snap.test-snapd-tools$' xdg-vars.txt
+ test $(wc -l < xdg-vars.txt) -ge 1
+debug: |
+ cat *-vars.txt
--- /dev/null
+summary: Check that `snap get` works as expected
+
+prepare: |
+ echo "Build basic test package (without hooks)"
+ snapbuild $TESTSLIB/snaps/basic .
+ snap install --dangerous basic_1.0_all.snap
+
+ echo "Build package with hook to run snapctl set"
+ snapbuild $TESTSLIB/snaps/snapctl-hooks .
+ snap install --dangerous snapctl-hooks_1.0_all.snap
+
+restore: |
+ rm basic_1.0_all.snap
+ rm snapctl-hooks_1.0_all.snap
+
+execute: |
+ echo "Test that snap get fails on a snap without any hooks"
+ if output=$(snap get basic foo); then
+ echo "snap get unexpectedly worked with output '$output'"
+ exit 1
+ fi
+
+ echo "Test that values set via snapctl can be obtained via snap get"
+ if ! snap set snapctl-hooks command=test-snapctl-set-foo; then
+ echo "snap set unexpectedly failed"
+ exit 1
+ fi
+ if ! output=$(snap get snapctl-hooks command); then
+ echo "snap get unexpectedly failed"
+ exit 1
+ fi
+ expected="test-snapctl-set-foo"
+ if [ "$output" != "$expected" ]; then
+ echo "Expected 'command' to be '$expected', but it was '$output'"
+ exit 1
+ fi
+ if ! output=$(snap get snapctl-hooks foo); then
+ echo "snap get unexpectedly failed"
+ exit 1
+ fi
+ expected="bar"
+ if [ "$output" != "$expected" ]; then
+ echo "Expected 'foo' to be '$expected', but it was '$output'"
+ exit 1
+ fi
--- /dev/null
+import os
+import re
+import sys
+import yaml
+
+def die(s):
+ print(s, file=sys.stderr)
+ sys.exit(1)
+
+def equals(name, s1, s2):
+ if s1 != s2:
+ die("in %s expected %r, got %r" % (name, s2, s1))
+
+def matches(name, s, r):
+ if not re.search(r, s):
+ die("in %s expected to match %s, got %r" % (name, r, s))
+
+def check(name, d, *a):
+ ka = set()
+ for k, op, *args in a:
+ if k not in d:
+ die("in %s expected to have a key %r" % (name, k))
+ op(name+"."+k, d[k], *args)
+ ka.add(k)
+ kd = set(d)
+ if ka < kd:
+ die("in %s: extra keys: %r" % (name, kd-ka))
+
+def exists(name, d):
+ pass
+
+verNotesRx = re.compile(r"^\w\S*\s+-$")
+def verRevNotesRx(s):
+ return re.compile(r"^\w\S*\s+\(\d+\)\s+[1-9][0-9]*\w+\s+" + s + "$")
+
+res = list(yaml.load_all(sys.stdin))
+
+equals("number of entries", len(res), 6)
+
+check("basic", res[0],
+ ("name", equals, "basic"),
+ ("summary", equals, "Basic snap"),
+ ("path", matches, r"^basic_[0-9.]+_all\.snap$"),
+ ("version", matches, verNotesRx),
+)
+
+check("basic-desktop", res[1],
+ ("name", equals, "basic-desktop"),
+ ("path", matches, "snaps/basic-desktop/$"), # note the trailing slash
+ ("summary", equals, ""),
+ ("version", matches, verNotesRx),
+)
+
+check("test-snapd-tools", res[2],
+ ("name", equals, "test-snapd-tools"),
+ ("publisher", equals, "canonical"),
+ ("summary", equals, "Tools for testing the snapd application"),
+ ("description", equals, "A tool to test snapd\n"),
+ ("commands", exists),
+ ("tracking", equals, "stable"),
+ ("installed", matches, verRevNotesRx("-")),
+ ("refreshed", exists),
+ ("channels", check,
+ ("stable", matches, verRevNotesRx("-")),
+ ("candidate", matches, verRevNotesRx("-")),
+ ("beta", matches, verRevNotesRx("-")),
+ ("edge", matches, verRevNotesRx("-")),
+ ),
+)
+
+check("test-snapd-devmode", res[3],
+ ("name", equals, "test-snapd-devmode"),
+ ("publisher", equals, "canonical"),
+ ("summary", equals, "Basic snap with devmode confinement"),
+ ("description", equals, "A basic buildable snap that asks for devmode confinement\n"),
+ ("tracking", equals, "beta"),
+ ("installed", matches, verRevNotesRx("devmode")),
+ ("refreshed", exists),
+ ("channels", check,
+ ("beta", matches, verRevNotesRx("devmode")),
+ ("edge", matches, verRevNotesRx("devmode")),
+ ),
+)
+
+check("core", res[4],
+ ("name", equals, "core"),
+ ("type", equals, "core"), # attenti al cane
+ ("publisher", exists),
+ ("summary", exists),
+ ("description", exists),
+ ("tracking", exists),
+ ("installed", exists),
+ ("refreshed", exists),
+ ("channels", exists),
+)
+
+check("error", res[5],
+ ("argument", equals, "/etc/passwd"),
+ ("warning", equals, "not a valid snap"),
+)
--- /dev/null
+summary: Check that snap info works
+
+prepare: |
+ apt-get install -y python3-yaml
+ snapbuild $TESTSLIB/snaps/basic .
+ snap install test-snapd-tools
+ snap install --channel beta --devmode test-snapd-devmode
+
+restore: |
+ rm basic_1.0_all.snap
+ snap remove test-snapd-tools test-snapd-devmode
+
+execute: |
+ echo "With no arguments, errors out"
+ snap info && exit 1 || true
+
+ echo "With one non-snap argument, errors out"
+ snap info /etc/passwd && exit 1 || true
+
+ snap info basic_1.0_all.snap $TESTSLIB/snaps/basic-desktop test-snapd-tools test-snapd-devmode core /etc/passwd > out
+ python3 check.py < out
--- /dev/null
+summary: Ensure remove with unmounted base dir works
+
+execute: |
+ cp -ar $TESTSLIB/snaps/test-snapd-tools /tmp
+ snap try /tmp/test-snapd-tools
+
+ # simulate what happens if someone "snap try /tmp/something" and then
+ # reboots: the dir is gone and nothing can be mounted anymore
+ rm -rf /tmp/test-snapd-tools
+ umount /snap/test-snapd-tools/x1
+
+ # ensure removal still works
+ snap remove test-snapd-tools
\ No newline at end of file
--- /dev/null
+summary: Check that alias symlinks work correctly
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+prepare: |
+ echo Ensure we have a os snap with snap run
+ $TESTSLIB/reset.sh
+ snap install --channel=beta core
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+restore:
+ rm -f /snap/bin/test_echo
+ rm -f /snap/bin/test_cat
+
+environment:
+ APP/testsnapdtoolsecho: test-snapd-tools.echo
+ APP/testsnapdtoolscat: test-snapd-tools.cat
+ ALIAS/testsnapdtoolsecho: test_echo
+ ALIAS/testsnapdtoolscat: test_cat
+ SNAP: /snap/test-snapd-tools/current
+
+execute: |
+ echo Testing that creating an alias symlinks works
+ $APP $SNAP/bin/cat
+ $APP $SNAP/bin/cat > orig.txt 2>&1
+
+ ln -s $APP /snap/bin/$ALIAS
+
+ $ALIAS $SNAP/bin/cat
+ $ALIAS $SNAP/bin/cat > new.txt 2>&1
+
+ diff -u orig.txt new.txt
--- /dev/null
+summary: Check that `snap run` can actually run hooks
+
+environment:
+ # Ensure that running purely from the deb (without re-exec) works
+ # correctly
+ SNAP_REEXEC/reexec0: 0
+ SNAP_REEXEC/reexec1: 1
+
+prepare: |
+ echo "Build test hooks package"
+ snapbuild $TESTSLIB/snaps/basic-hooks .
+ snap install --dangerous basic-hooks_1.0_all.snap
+
+restore: |
+ rm basic-hooks_1.0_all.snap
+
+restore: |
+ rm basic-hooks_1.0_all.snap
+
+execute: |
+ # Note that `snap run` doesn't exit non-zero if the hook is missing, so we
+ # check the output instead.
+
+ echo "Test that snap run can call valid hooks"
+
+ if ! output="$(snap run --hook=configure basic-hooks)"; then
+ echo "Failed to run configure hook"
+ exit 1
+ fi
+
+ expected_output="configure hook"
+ if [ "$output" != "$expected_output" ]; then
+ echo "Expected configure output to be '$expected_output', but it was '$output'"
+ exit 1
+ fi
+
+ echo "Test that snap run cannot call invalid hooks"
+
+ if output="$(snap run --hook=invalid-hook basic-hooks)"; then
+ echo "Expected snap run to fail upon missing hook, but it was '$output'"
+ exit 1
+ fi
+
+ expected_output=""
+ if [ "$output" != "$expected_output" ]; then
+ echo "Expected invalid-hook output to be '$expected_output', but it was '$output'"
+ exit 1
+ fi
--- /dev/null
+summary: Check error handling in symlinks to /usr/bin/snap
+restore: |
+ rm -f /snap/bin/xxx
+ rmdir /snap/bin
+execute: |
+ echo Setting up incorrect symlink for snap run
+ mkdir -p /snap/bin
+ ln -s /usr/bin/snap /snap/bin/xxx
+ echo Running unknown command
+ expected='internal error, please report: running "xxx" failed: cannot find current revision for snap xxx: readlink /snap/xxx/current: no such file or directory'
+ output="$(/snap/bin/xxx 2>&1 )" && exit 1
+ echo $output
+ err=$?
+ echo Verifying error message
+ if [ $err -ne 46 ]; then
+ echo Wrong error code $err
+ fi
+ [ "$output" = "$expected" ] || exit 1
+
--- /dev/null
+summary: Check that symlinks to /usr/bin/snap trigger `snap run`
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+prepare: |
+ echo Ensure we have a os snap with snap run
+ $TESTSLIB/reset.sh
+ snap install --channel=beta core
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+
+environment:
+ APP/testsnapdtoolsecho: test-snapd-tools.echo
+ APP/testsnapdtoolscat: test-snapd-tools.cat
+ SNAP: /snap/test-snapd-tools/current
+
+execute: |
+ echo Testing that replacing the wrapper with a symlink works
+ $APP $SNAP/bin/cat
+ $APP $SNAP/bin/cat > orig.txt 2>&1
+
+ rm /snap/bin/$APP
+ ln -s /usr/bin/snap /snap/bin/$APP
+
+ $APP $SNAP/bin/cat
+ $APP $SNAP/bin/cat > new.txt 2>&1
+
+ diff -u orig.txt new.txt
--- /dev/null
+summary: Check that `snap set` runs configure hook.
+
+prepare: |
+ echo "Build basic test package (without hooks)"
+ snapbuild $TESTSLIB/snaps/basic .
+ snap install --dangerous basic_1.0_all.snap
+
+ echo "Build failing hooks package"
+ snapbuild $TESTSLIB/snaps/failing-config-hooks .
+
+ echo "Build package with hook to run snapctl set"
+ snapbuild $TESTSLIB/snaps/snapctl-hooks .
+ snap install --dangerous snapctl-hooks_1.0_all.snap
+
+restore: |
+ rm basic_1.0_all.snap
+ rm failing-config-hooks_1.0_all.snap
+ rm snapctl-hooks_1.0_all.snap
+
+execute: |
+ echo "Test that snap set fails without configure hook"
+ if snap set basic foo=bar; then
+ echo "Expected snap set to fail without a configure hook"
+ exit 1
+ fi
+
+ echo "Test that snap set fails when configure hook fails"
+ if snap set snapctl-hooks command=test-exit-one; then
+ echo "Expected snap set to fail when configure hook fails"
+ exit 1
+ fi
+
+ echo "Test that the set value can be retrieved by the hook"
+ if ! snap set snapctl-hooks command=test-snapctl-get-foo foo=bar; then
+ echo "Expected hook to be able to retrieve set value"
+ exit 1
+ fi
+
+ echo "Install should fail altogether as it has a broken hook"
+ if obtained=$(snap install --dangerous failing-config-hooks_1.0_all.snap 2>&1); then
+ echo "Expected install of snap with broken configure hook to fail"
+ exit 1
+ fi
+ [[ "$obtained" == *"error from within configure hook"* ]]
--- /dev/null
+spawn snap create-key
+
+expect "Passphrase: "
+send "pass\n"
+
+expect "Confirm passphrase: "
+send "pass\n"
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
+
+set timeout 60
+
--- /dev/null
+spawn bash
+
+expect {
+ "# " { send "cat pi3-model.json | snap sign -k default &> pi3.model ; cat pi3.model\n" }
+}
+
+# fun!
+# gpg1 asks for a passphrase on the terminal no matter what
+# gpg2 gets the passphrase via our fake pinentry
+expect {
+ "Enter passphrase: " {send "pass\n"; exp_continue}
+ "type: model" { send "exit\n" }
+ timeout { exit 1 }
+ eof { exit 1 }
+}
+
+set status [wait]
+if {[lindex $status 3] != 0} {
+ exit 1
+}
--- /dev/null
+summary: Run snap sign to sign a model assertion
+
+# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2502
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32, -ubuntu-16.04-ppc64el, -ubuntu-16.10-ppc64el, -ubuntu-17.04-ppc64el]
+
+prepare: |
+ . "$TESTSLIB/mkpinentry.sh"
+
+execute: |
+ echo "Creating a new key without a password"
+ expect -f create-key.exp
+
+ echo "Ensure we have the new key"
+ snap keys|grep default
+ key=$(snap keys|grep default|tr -s ' ' |cut -f2 -d' ')
+
+ echo "Create an example model assertion"
+ cat <<EOF >pi3-model.json
+ {
+ "type": "model",
+ "authority-id": "test",
+ "brand-id": "test",
+ "series": "16",
+ "model": "pi3",
+ "architecture": "armhf",
+ "gadget": "pi3",
+ "kernel": "pi2-kernel",
+ "timestamp": "$(date --utc '+%FT%T%:z')"
+ }
+ EOF
+ echo "Sign the model assertion with our key"
+ expect -d -f sign-model.exp
+
+ echo "Verify that the resulting model assertion is signed"
+ grep "sign-key-sha3-384: $key" pi3.model
--- /dev/null
+summary: Check that `snapctl` can be run from within hooks
+
+prepare: |
+ snapbuild $TESTSLIB/snaps/snapctl-hooks .
+ snap install --dangerous snapctl-hooks_1.0_all.snap
+
+restore: |
+ rm snapctl-hooks_1.0_all.snap
+
+execute: |
+ echo "Verify that snapctl -h runs without a context"
+ if ! snapctl -h; then
+ echo "Expected snapctl -h to be successful"
+ fi
+
+ echo "Verify that the snapd API is only available via the snapd socket"
+ if ! printf "GET /v2/snaps HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket | grep "200 OK"; then
+ echo "Expected snapd API to be available on the snapd socket"
+ echo "Got: $(curl -s --unix-socket /run/snapd.socket http:/v2/snaps)"
+ exit 1
+ fi
+
+ if ! printf "GET /v2/snaps HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd-snap.socket | grep "401 Unauthorized"; then
+ echo "Expected snapd API to be unauthorized on the snap socket"
+ exit 1
+ fi
--- /dev/null
+summary: Test that snapd reexecs itself into core
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+environment:
+ SNAPD_CONF: /etc/systemd/system/snapd.service.d/local.conf
+
+restore: |
+ # extra cleanup in case something in this test went wrong
+ rm -f /etc/systemd/system/snapd.service.d/no-reexec.conf
+ systemctl stop snapd.service snapd.socket
+ if mount|grep "/snap/core/.*/usr/lib/snapd/info"; then
+ umount /snap/core/current/usr/lib/snapd/info
+ fi
+ if mount|grep "/snap/core/.*/usr/lib/snapd/snapd"; then
+ umount /snap/core/current/usr/lib/snapd/snapd
+ fi
+ rm -f /tmp/old-info
+
+execute: |
+ if [ "${SNAP_REEXEC:-}" = "0" ]; then
+ echo "skipping test when SNAP_REEXEC is disabled"
+ exit 0
+ fi
+
+ echo "Ensure we re-exec by default"
+ snap list
+ journalctl | MATCH "DEBUG: restarting into"
+
+ echo "Ensure that we do not re-exec into older versions"
+ systemctl stop snapd.service snapd.socket
+ echo "mount something older than our freshly build snapd"
+ echo "VERSION=1.0">/tmp/old-info
+ mount --bind /tmp/old-info /snap/core/current/usr/lib/snapd/info
+ systemctl start snapd.service snapd.socket
+ snap list
+ journalctl | MATCH "not restarting into.*older than"
+
+ echo "Revert back to normal"
+ systemctl stop snapd.service snapd.socket
+ umount /snap/core/current/usr/lib/snapd/info
+
+ echo "Ensure SNAP_REEXEC=0 is honored for snapd"
+ cat > /etc/systemd/system/snapd.service.d/no-reexec.conf <<EOF
+ [Service]
+ Environment=SNAP_REEXEC=0
+ EOF
+ echo "Breaking snapd, copy to tmp to ensure mtime is newer"
+ cat > /tmp/broken-snapd <<EOF
+ #!/bin/sh
+ echo "from the core snap"
+ exit 1
+ EOF
+ chmod +x /tmp/broken-snapd
+ mount --bind /tmp/broken-snapd /snap/core/current/usr/lib/snapd/snapd
+ systemctl daemon-reload
+ systemctl start snapd.service snapd.socket
+ echo "Ensure that snap list works normally"
+ echo "(i.e. the snapd from the core image is not run)"
+ snap list | MATCH core
+
+ echo "Revert back to normal"
+ systemctl stop snapd.service snapd.socket
+ umount /snap/core/current/usr/lib/snapd/snapd
+ rm -f /etc/systemd/system/snapd.service.d/no-reexec.conf
+ systemctl daemon-reload
+ systemctl start snapd.service snapd.socket
+
+ echo "Ensure SNAP_REEXEC=0 is honored for snap"
+ mount --bind /tmp/broken-snapd /snap/core/current/usr/bin/snap
+ snap list|MATCH "from the core snap"
+ SNAP_REEXEC=0 snap list|MATCH "core"
+ umount /snap/core/current/usr/bin/snap
+
+ echo "Ensure a core refresh restart snapd"
+ . $TESTSLIB/names.sh
+ prev_core=$(snap list | awk "/^${core_name} / {print(\$3)}")
+ snap install --dangerous /var/lib/snapd/snaps/${core_name}_${prev_core}.snap
+ journalctl | MATCH "Requested daemon restart"
+
+ echo "Ensure the right snapd (from the new core) is running"
+ now_core=$(snap list | awk "/^${core_name} / {print(\$3)}")
+ if [ "$now_core" = "$prev_core" ]; then
+ echo "Test broken $now_core and $prev_Core are the same"
+ exit 1
+ fi
+ SNAPD_PATH=$(readlink -f /proc/$(pidof snapd)/exe)
+ if [ "$SNAPD_PATH" != "/snap/${core_name}/${now_core}/usr/lib/snapd/snapd" ]; then
+ echo "unexpected $SNAPD_PATH for $now_core snap (previous $prev_core)"
+ exit 1
+ fi
--- /dev/null
+summary: Check that snapd can be built without cgo
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+execute: |
+ CGO_ENABLED=0 go build -o snapd.static github.com/snapcore/snapd/cmd/snapd
+ ldd snapd.static && exit 1 || true
--- /dev/null
+summary: Check that a service installed by a snap is reported as active by systemd
+
+environment:
+ SERVICE_NAME: snap.network-bind-consumer.network-consumer.service
+
+execute: |
+ echo "Given a service snap is installed"
+ snapbuild $TESTSLIB/snaps/network-bind-consumer .
+ snap install --dangerous network-bind-consumer_1.0_all.snap
+
+ echo "When the service state is reported as active"
+ while ! systemctl show -p ActiveState $SERVICE_NAME | grep -Pq "ActiveState=active"; do sleep 0.5; done
+
+ echo "Then systemctl reports the status of the service as loaded, active and running"
+ expected="(?s).*?Loaded: loaded .*?Active: active \(running\)"
+ systemctl status $SERVICE_NAME | grep -Pqz "$expected"
--- /dev/null
+summary: Checks that removing the base directory of a tried snap works.
+
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+execute: |
+ echo "Given a tried snap"
+ base_dir=$(mktemp -d)
+ cp -R $TESTSLIB/snaps/test-snapd-tools/* $base_dir
+ snap try $base_dir
+
+ echo "Then it is listed as installed"
+ snap list | grep -Pq "^test-snapd-tools +.*?try"
+
+ echo "When its base directory is removed"
+ rm -rf $base_dir
+
+ echo "Then the snap is listed as a broken install"
+ snap list | grep -Pq "^test-snapd-tools +.*?broken"
--- /dev/null
+summary: Check that snaps vanishing are handled gracefully
+environment:
+ SNAP_NAME/test_snapd_tools: test-snapd-tools
+ SNAP_NAME/network_bind_consumer: network-bind-consumer
+ TRYDIR: $(pwd)/trydir
+
+restore: |
+ rm -rf $TRYDIR
+
+execute: |
+ mkdir -p $TRYDIR
+
+ cp -ar $TESTSLIB/snaps/$SNAP_NAME/* $TRYDIR
+
+ echo Trying a snap
+ snap try $TRYDIR
+ snap list |grep $SNAP_NAME
+
+ echo Removing a snap try dir does not break everything
+ rm -rf $TRYDIR
+ snap list |grep core
+
+ echo A snap in broken state can be removed
+ snap remove $SNAP_NAME
+
+ echo And is gone afterwards
+ snap list |grep -v $SNAP_NAME
+
+ echo And all its binaries
+ N="$(ls /snap/bin/$SNAP_NAME*|wc -l)"
+ if [ "$N" -ne 0 ]; then
+ echo "Some binaries are not cleaned"
+ exit 1
+ fi
+
+ echo And all its services
+ N="$(ls /etc/systemd/system/snap.$SNAP_NAME.*|wc -l)"
+ if [ "$N" -ne 0 ]; then
+ echo "Some services are not cleaned"
+ exit 1
+ fi
--- /dev/null
+summary: Check that try command works when snap dir is omitted
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+execute: |
+ echo "When try is executed inside a snap directory"
+ cd $TESTSLIB/snaps/test-snapd-tools
+ snap try
+
+ echo "Then the snap is listed as installed with try in the notes"
+ snap list | grep -Pq "^test-snapd-tools +.*?try"
--- /dev/null
+summary: Check that try command works when daemon line is added to snap.yaml
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+
+environment:
+ SERVICE: snap.test-snapd-service-try.service.service
+
+execute: |
+ echo "Given a buildable snap in a known directory"
+ echo "When try is executed on that directory"
+ snap try $TESTSLIB/snaps/test-snapd-service-try-v1
+
+ echo "Then the snap is listed as installed with try in the notes"
+ snap list | grep -Pq "^test-snapd-service-try +.*?try"
+
+ # Sanity check just to ensure the test and its input yaml are correct
+ echo "And it doesn't have a systemd service just yet"
+ # Note: systemctl in yakkety doesn't report LoadState=not-found for unknown services
+ # so need to grep all the services
+ if systemctl|grep -q $SERVICE ; then
+ echo "Unexpected service $SERVICE"
+ exit 1
+ fi
+
+ echo "When snap.yaml is updated with the daemon line added"
+ echo "Then it can be tried again"
+ snap try $TESTSLIB/snaps/test-snapd-service-try-v2
+ snap list | grep -Pq "^test-snapd-service-try +.*?try"
+ echo "And a service is now loaded with systemd"
+
+ expected_output="LoadState=loaded"
+ output=$(systemctl show --property=LoadState $SERVICE)
+ if [ "$output" != "$expected_output" ]; then
+ echo "Expected systemctl show output to be '$expected_output', but it was '$output'"
+ exit 1
+ fi
--- /dev/null
+summary: Check that try command works
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+environment:
+ PORT: 8081
+ SERVICE_FILE: "./service.sh"
+ READABLE_FILE: "/var/snap/test-snapd-tools/x1/file.txt"
+ SERVICE_NAME: "test-service"
+
+prepare: |
+ . $TESTSLIB/systemd.sh
+ echo "Given a service listening on a port"
+ printf "#!/bin/sh -e\nwhile true; do printf \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE
+ chmod a+x $SERVICE_FILE
+ systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)"
+
+ while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done
+
+restore: |
+ . $TESTSLIB/systemd.sh
+ systemd_stop_and_destroy_unit $SERVICE_NAME
+ rm -f $SERVICE_FILE $READABLE_FILE
+
+execute: |
+ echo "Given a buildable snap in a known directory"
+ echo "When try is executed on that directory"
+ snap try $TESTSLIB/snaps/test-snapd-tools
+
+ echo "Then the snap is listed as installed with try in the notes"
+ snap list | grep -Pq "^test-snapd-tools +.*?try"
+
+ echo "And commands from the snap-try binary can be run"
+ test-snapd-tools.success
+
+ echo "And commands from the snap-try binary can read in a readable dir"
+ echo -n "Hello World" > $READABLE_FILE
+ test-snapd-tools.cat $READABLE_FILE | grep -q "Hello World"
+
+ echo "====================================="
+
+ echo "Given a buildable snap which access confinement-protected resources in a known directory"
+ echo "When try is executed on that directory"
+ snap try $TESTSLIB/snaps/test-snapd-tools
+
+ echo "Then the snap command is not able to access the protected resource"
+ if test-snapd-tools.head -1 /dev/kmsg; then
+ echo "Expected confinement denial in try mode didn't work"
+ exit 1
+ fi
+
+ echo "====================================="
+
+ echo "Given a buildable snap which access confinement-protected resources in a known directory"
+ echo "When try is executed on that directory with devmode enabled"
+ snap try $TESTSLIB/snaps/test-snapd-tools --devmode
+
+ echo "Then the snap command is able to access the protected resource"
+ test-snapd-tools.head -1 /dev/kmsg
+
+ echo "====================================="
+
+ echo "Given a buildable snap which access confinement-enabled network resources in a known directory"
+ echo "When try is executed on that directory"
+ snap try $TESTSLIB/snaps/network-consumer
+
+ echo "Then the snap is able to access the network resource"
+ network-consumer http://127.0.0.1:$PORT | grep -q "ok"
--- /dev/null
+summary: Ensure that the apt output on ubuntu-core is correct
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+execute: |
+ expected="Ubuntu Core does not use apt-get, see 'snap --help'!"
+ output=$(apt-get update)
+ if [ "$output" != "$expected" ]; then
+ echo "Unexpected apt output: $output"
+ exit 1
+ fi
--- /dev/null
+summary: Ensure classic dimension works correctly
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+environment:
+ # We need to set the SUDO_USER here to simulate the real
+ # behavior. I.e. when entering classic it happens via
+ # `sudo classic` and the user gets a user shell inside
+ # the classic environment that has sudo support.
+ SUDO_USER: test
+prepare: |
+ echo "test ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/create-test
+restore: |
+ rm -f /etc/sudoers.d/create-test
+execute: |
+ echo "Ensure classic can be installed"
+ snap install --devmode --beta classic
+ snap list|grep classic
+
+ echo "Check that classic can run commands inside classic"
+ classic test -f /var/lib/dpkg/status
+
+ echo "Ensure that after classic exits no processes are left behind"
+ classic "sleep 133713371337&"
+ if ps afx|grep 133713371337|grep -v grep; then
+ echo "The sleep process was not killed when classic exited"
+ echo "Something is wrong with the cleanup"
+ exit 1
+ fi
+
+ echo "Ensure sudo works without a password inside classic"
+ # classic uses "script" to work around the issue that
+ # tty reports "no tty" inside snaps (LP: #1611493)
+ #
+ # "script" adds extra \r into the output that we need to filter here
+ if [ "$(classic sudo id -u|tr -d "\r")" != "0" ]; then
+ echo "sudo inside classic did not work as expected"
+ exit 1
+ fi
+
+ for d in /proc /run /sys /dev /snappy; do
+ if ! classic test -d $d; then
+ echo "Expected dir $d is missing inside classic"
+ exit 1
+ fi
+ if ! classic mount | grep "$d"; then
+ echo "Expected bind mount for $d in classic missing"
+ exit 1
+ fi
+ done
--- /dev/null
+summary: Ensure that snap create-user works in ubuntu-core
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+restore: |
+ # meh, deluser has no --extrausers support
+ sed -i '/^mvo/d' /var/lib/extrausers/passwd
+ sed -i '/^mvo/d' /var/lib/extrausers/shadow
+ sed -i '/^mvo/d' /var/lib/extrausers/group
+ rm -rf /home/mvo
+ rm -f create.error
+execute: |
+ if [ "$MANAGED_DEVICE" = "true" ]; then
+ if snap create-user --sudoer mvo@ubuntu.com 2>create.error; then
+ echo "Did not get expected error creating user in managed device"
+ exit 1
+ fi
+ grep "cannot create user: device already managed" create.error
+ exit 0
+ fi
+ echo "Adding valid user"
+ expected='created user "mvo"'
+ output=$(snap create-user --sudoer mvo@ubuntu.com)
+ if [ "$output" != "$expected" ]; then
+ echo "Unexpected output $output"
+ exit 1
+ fi
+ echo "Ensure there are ssh keys imported"
+ grep ssh-rsa /home/mvo/.ssh/authorized_keys
+
+ echo "Ensure the user is a sudo user"
+ sudo -u mvo sudo true
+
+
+ echo "Adding invalid user"
+ expected='error: while creating user: cannot create user "nosuchuser@example.com"'
+ if output=$(snap create-user nosuchuser@example.com); then
+ echo "snap create-user should fail for unknown users but it did not"
+ exit 1
+ fi
+ if !echo $output|grep expected; then
+ echo "Unexpected output $output"
+ exit 1
+ fi
--- /dev/null
+import sys
+import yaml
+
+with open(sys.argv[1]) as f:
+ seed = yaml.load(f)
+
+i = 0
+snaps = seed['snaps']
+while i < len(snaps):
+ entry = snaps[i]
+ if entry['name'] == 'pc':
+ snaps[i] = {
+ "name": "pc",
+ "unasserted": True,
+ "file": "pc_x1.snap",
+ }
+ break
+ i += 1
+
+with open(sys.argv[1], 'w') as f:
+ yaml.dump(seed, stream=f, indent=2, default_flow_style=False)
--- /dev/null
+#!/bin/sh
+snapctl set device-service.url=http://localhost:11029
+snapctl set device-service.headers='{"X-Use-Proposed": "yes"}'
+snapctl set registration.proposed-serial="Y1234"
+snapctl set registration.body='mac: "00:00:00:00:ff:00"'
--- /dev/null
+summary: |
+ Test that device initialisation and registration can be customized
+ with the prepare-device gadget hook and this can set request headers,
+ a proposed serial and the body of the serial assertion
+systems: [ubuntu-core-16-64]
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ . $TESTSLIB/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ rm -rf /var/lib/snapd/assertions/*
+ rm -rf /var/lib/snapd/device
+ rm -rf /var/lib/snapd/state.json
+ unsquashfs /var/lib/snapd/snaps/pc_*.snap
+ mkdir -p squashfs-root/meta/hooks
+ cp prepare-device squashfs-root/meta/hooks
+ mksquashfs squashfs-root pc_x1.snap -comp xz
+ rm -rf squashfs-root
+ cp pc_x1.snap /var/lib/snapd/seed/snaps/
+ mv /var/lib/snapd/seed/assertions/model model.bak
+ cp /var/lib/snapd/seed/seed.yaml seed.yaml.bak
+ python3 ./manip_seed.py /var/lib/snapd/seed/seed.yaml
+ cp $TESTSLIB/assertions/developer1.account /var/lib/snapd/seed/assertions
+ cp $TESTSLIB/assertions/developer1.account-key /var/lib/snapd/seed/assertions
+ cp $TESTSLIB/assertions/developer1-pc.model /var/lib/snapd/seed/assertions
+ cp $TESTSLIB/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions
+ # start fake device svc
+ systemd_create_and_start_unit fakedevicesvc "$(which fakedevicesvc) localhost:11029"
+ # kick first boot again
+ systemctl start snapd.service snapd.socket
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ . $TESTSLIB/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ systemd_stop_and_destroy_unit fakedevicesvc
+ rm -rf /var/lib/snapd/assertions/*
+ rm -rf /var/lib/snapd/device
+ rm -rf /var/lib/snapd/state.json
+ if systemctl status snap-pc-x1.mount ; then
+ systemctl stop snap-pc-x1.mount
+ rm -f /etc/systemd/system/snap-pc-x1.mount
+ rm -f /etc/systemd/system/multi-user.target.wants/snap-pc-x1.mount
+ rm -f /var/lib/snapd/snaps/pc_x1.snap
+ systemctl daemon-reload
+ fi
+ rm /var/lib/snapd/seed/snaps/pc_x1.snap
+ cp seed.yaml.bak /var/lib/snapd/seed/seed.yaml
+ rm /var/lib/snapd/seed/assertions/developer1.account
+ rm /var/lib/snapd/seed/assertions/developer1.account-key
+ rm /var/lib/snapd/seed/assertions/developer1-pc.model
+ rm /var/lib/snapd/seed/assertions/testrootorg-store.account-key
+ cp model.bak /var/lib/snapd/seed/assertions/model
+ rm -f *.bak
+ # kick first boot again
+ systemctl start snapd.service snapd.socket
+ # wait for first boot to be done
+ while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Wait for first boot to be done"
+ while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done
+ echo "We have a model assertion"
+ snap known model|grep "model: my-model"
+
+ echo "Wait for device initialisation to be done"
+ while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done
+
+ echo "Check we have a serial"
+ snap known serial|grep "authority-id: developer1"
+ snap known serial|grep "brand-id: developer1"
+ snap known serial|grep "model: my-model"
+ snap known serial|grep "serial: Y1234"
+ snap known serial|grep 'mac: "00:00:00:00:ff:00"'
--- /dev/null
+import sys
+import yaml
+
+with open(sys.argv[1]) as f:
+ seed = yaml.load(f)
+
+i = 0
+snaps = seed['snaps']
+while i < len(snaps):
+ entry = snaps[i]
+ if entry['name'] == 'pc':
+ snaps[i] = {
+ "name": "pc",
+ "unasserted": True,
+ "file": "pc_x1.snap",
+ }
+ break
+ i += 1
+
+with open(sys.argv[1], 'w') as f:
+ yaml.dump(seed, stream=f, indent=2, default_flow_style=False)
--- /dev/null
+#!/bin/sh
+snapctl set device-service.url=http://localhost:11029
--- /dev/null
+summary: |
+ Test that device initialisation and registration can be customized
+ with the prepare-device gadget hook
+systems: [ubuntu-core-16-64]
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ . $TESTSLIB/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ rm -rf /var/lib/snapd/assertions/*
+ rm -rf /var/lib/snapd/device
+ rm -rf /var/lib/snapd/state.json
+ unsquashfs /var/lib/snapd/snaps/pc_*.snap
+ mkdir -p squashfs-root/meta/hooks
+ cp prepare-device squashfs-root/meta/hooks
+ mksquashfs squashfs-root pc_x1.snap -comp xz
+ rm -rf squashfs-root
+ cp pc_x1.snap /var/lib/snapd/seed/snaps/
+ mv /var/lib/snapd/seed/assertions/model model.bak
+ cp /var/lib/snapd/seed/seed.yaml seed.yaml.bak
+ python3 ./manip_seed.py /var/lib/snapd/seed/seed.yaml
+ cp $TESTSLIB/assertions/developer1.account /var/lib/snapd/seed/assertions
+ cp $TESTSLIB/assertions/developer1.account-key /var/lib/snapd/seed/assertions
+ cp $TESTSLIB/assertions/developer1-pc.model /var/lib/snapd/seed/assertions
+ cp $TESTSLIB/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions
+ # start fake device svc
+ systemd_create_and_start_unit fakedevicesvc "$(which fakedevicesvc) localhost:11029"
+ # kick first boot again
+ systemctl start snapd.service snapd.socket
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ . $TESTSLIB/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ systemd_stop_and_destroy_unit fakedevicesvc
+ rm -rf /var/lib/snapd/assertions/*
+ rm -rf /var/lib/snapd/device
+ rm -rf /var/lib/snapd/state.json
+ if systemctl status snap-pc-x1.mount ; then
+ systemctl stop snap-pc-x1.mount
+ rm -f /etc/systemd/system/snap-pc-x1.mount
+ rm -f /etc/systemd/system/multi-user.target.wants/snap-pc-x1.mount
+ rm -f /var/lib/snapd/snaps/pc_x1.snap
+ systemctl daemon-reload
+ fi
+ rm /var/lib/snapd/seed/snaps/pc_x1.snap
+ cp seed.yaml.bak /var/lib/snapd/seed/seed.yaml
+ rm /var/lib/snapd/seed/assertions/developer1.account
+ rm /var/lib/snapd/seed/assertions/developer1.account-key
+ rm /var/lib/snapd/seed/assertions/developer1-pc.model
+ rm /var/lib/snapd/seed/assertions/testrootorg-store.account-key
+ cp model.bak /var/lib/snapd/seed/assertions/model
+ rm -f *.bak
+ # kick first boot again
+ systemctl start snapd.service snapd.socket
+ # wait for first boot to be done
+ while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ echo "Wait for first boot to be done"
+ while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done
+ echo "We have a model assertion"
+ snap known model|grep "model: my-model"
+
+ echo "Wait for device initialisation to be done"
+ while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done
+
+ echo "Check we have a serial"
+ snap known serial|grep "authority-id: developer1"
+ snap known serial|grep "brand-id: developer1"
+ snap known serial|grep "model: my-model"
+ snap known serial|grep "serial: 7777"
--- /dev/null
+summary: |
+ Ensure after device initialisation registration worked and
+ we have a serial and can acquire a session macaroon
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+execute: |
+ echo "Wait for first boot to be done"
+ while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done
+ echo "We have a model assertion"
+ snap known model|grep "series: 16"
+
+ if ! snap known model|grep "brand-id: canonical" ; then
+ echo "Not a canonical model. Skipping."
+ exit 0
+ fi
+
+ echo "Wait for device initialisation to be done"
+ while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done
+
+ echo "Check we have a serial"
+ snap known serial|grep "authority-id: canonical"
+ snap known serial|grep "brand-id: canonical"
+ if [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then
+ snap known serial|grep "model: pc"
+ fi
+
+ echo "Make sure we could acquire a session macaroon"
+ snap find pc
+ grep -qE '"session-macaroon":"[^"]' /var/lib/snapd/state.json
--- /dev/null
+summary: Test ubuntu-fan
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+prepare: |
+ IP=$(ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | cut -d' ' -f1|head -1)
+ fanctl up 241.0.0.0/8 $IP/16
+restore: |
+ fanctl down -e
+execute: |
+ echo "Test that fanctl exists"
+ fanctl |grep "Usage: /usr/sbin/fanctl <cmd>.*" || true
+
+ echo "Test fanctl created fan bridge"
+ ifconfig |grep ^fan-241
+
+ # FIXME: port the docker tests once we have docker again
+ # https://github.com/snapcore/snapd/blob/2.13/integration-tests/tests/ubuntufan_test.go#L88
--- /dev/null
+summary: Ensure we have no unpacked kernel.img/initrd.img on grub systems
+systems: [ubuntu-core-16-64]
+environment:
+ NAME/initrdimg: initrd.img*
+ NAME/kernelimg: kernel.img*
+ NAME/vmlinuz: vmlinuz*
+execute: |
+ output=$(find /boot/grub -name "$NAME" )
+ if [ -n "$output" ]; then
+ echo "found unexpected file $NAME: $output"
+ exit 1
+ fi
--- /dev/null
+summary: check that os-release is correct
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+execute: |
+ cat /etc/os-release
+ grep "ID=ubuntu-core" /etc/os-release
+
+ cat /etc/lsb-release
+ grep "DISTRIB_RELEASE=16" /etc/lsb-release
--- /dev/null
+summary: Ensure that service and apparmor profiles work after a reboot
+
+systems:
+ - ubuntu-core-16-64
+ - ubuntu-core-16-arm-64
+ - ubuntu-core-16-arm-32
+
+prepare: |
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ install_local network-bind-consumer
+
+execute: |
+ echo "Ensure snaps are (still) there."
+ snap list | grep test-snapd-tools
+ snap list | grep network-bind-consumer
+
+ echo "Ensure the service is (still) running."
+ retries=30
+ while ! systemctl is-active snap.network-bind-consumer.network-consumer.service; do
+ if [ $retries -eq 0 ]; then
+ echo "Service did not activate."
+ exit 1
+ fi
+ retries=$(( $retries - 1 ))
+ sleep 1
+ done
+
+ echo "Ensure apparmor profiles are (still) loaded."
+ for app in success fail echo head env block cat; do
+ grep "snap.test-snapd-tools.$app (enforce)" /sys/kernel/security/apparmor/profiles
+ done
+
+ if [ "$SPREAD_REBOOT" = "0" ]; then
+ REBOOT
+ fi
--- /dev/null
+summary: Ensure we have unpacked kernel.img/initrd.img on uboot systems
+systems: [ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+environment:
+ NAME/initrdimg: initrd.img*
+ NAME/kernelimg: kernel.img*
+
+execute: |
+ output=$(find /boot/uboot/*-kernel_*.snap/ -name "$NAME" )
+ if [ -z "$output" ]; then
+ echo "Not found expected file $NAME in /boot/uboot/*-kernel_*.snap/"
+ exit 1
+ fi
--- /dev/null
+summary: Upgrade the core snap a few times and ensure no GC happens
+
+systems:
+ - ubuntu-core-16-64-fixme
+ - ubuntu-core-16-arm-64
+ - ubuntu-core-16-arm-32
+
+debug: |
+ . $TESTSLIB/boot.sh
+ bootenv
+ ls /var/lib/snapd/snaps
+
+prepare: |
+ readlink /snap/core/current > current-core-symlink
+ readlink -f /snap/core/current > current-core-symlink-target
+
+restore: |
+ rm -f /snap/core/current
+ ln -s $(cat current-core-symlink) /snap/core/current
+ rm -f current-core-symlink
+ if [ $(readlink -f /snap/core/current) != $(cat current-core-symlink-target) ]; then
+ echo "failed to restore core symlink"
+ echo "$(readlink -f /snap/core/current) != $(cat current-core-symlink-target)"
+ exit 1
+ fi
+ shutdown -c
+ . $TESTSLIB/boot.sh
+ bootenv_unset snap_try_core
+
+execute: |
+ . $TESTSLIB/names.sh
+ . $TESTSLIB/boot.sh
+
+ echo "Get the current running core snap"
+ cur=$(bootenv snap_core)
+
+ echo "Install a bunch more core packages"
+ for i in $(seq 5); do
+ next=$(bootenv snap_try_core)
+ snap install --dangerous /var/lib/snapd/snaps/$cur
+ if [ "$next" = "$(bootenv snap_try_core)" ]; then
+ echo "The snap_try_core bootenv did not get updated"
+ exit 1
+ fi
+ done
+
+ echo "And verify that we did not garbage collect the current core"
+ if [ ! -e /var/lib/snapd/snaps/$cur ]; then
+ echo "The current core snap is no longer installed"
+ exit 1
+ fi
--- /dev/null
+summary: Upgrade the core snap and revert a few times
+
+systems:
+ - ubuntu-core-16-64-fixme
+ - ubuntu-core-16-arm-64
+ - ubuntu-core-16-arm-32
+
+debug: |
+ . $TESTSLIB/boot.sh
+ bootenv
+ cat /proc/cmdline
+
+restore: |
+ rm -f prevBoot nextBoot
+
+prepare: |
+ . $TESTSLIB/names.sh
+ snap list | awk "/^${core_name} / {print(\$3)}" > nextBoot
+
+execute: |
+ . $TESTSLIB/names.sh
+ . $TESTSLIB/boot.sh
+
+ # FIXME Why it starting with snap_mode=try the first time?
+ # Perhaps because core is installed after seeding? Do we
+ # want that on pristine images?
+ if [ $SPREAD_REBOOT != 0 ]; then
+ echo "Waiting for snapd to clean snap_mode"
+ while [ "$(bootenv snap_mode)" != "" ]; do
+ sleep 1
+ done
+
+ echo "Ensure the bootloader is correct after reboot"
+ test "$(bootenv snap_core)" = "${core_name}_$(cat nextBoot).snap"
+ test "$(bootenv snap_try_core)" = ""
+ test "$(bootenv snap_mode)" = ""
+ fi
+
+ snap list | awk "/^${core_name} / {print(\$3)}" > prevBoot
+
+ case $SPREAD_REBOOT in
+
+ 0) snap install --dangerous /var/lib/snapd/snaps/${core_name}_$(cat prevBoot).snap ;;
+ 1) snap revert ${core_name} ;;
+ 2) snap install --dangerous /var/lib/snapd/snaps/${core_name}_$(cat prevBoot).snap ;;
+ 3) snap revert ${core_name} ;;
+ 4) exit 0 ;;
+
+ esac
+
+ echo "Ensure the bootloader is correct before reboot"
+ snap list | awk "/^${core_name} / {print(\$3)}" > nextBoot
+ test "$(cat prevBoot)" != "$(cat nextBoot)"
+ test "$(bootenv snap_try_core)" = "${core_name}_$(cat nextBoot).snap"
+ test "$(bootenv snap_mode)" = "try"
+
+ echo "Ensure the device is scheduled for auto-reboot"
+ output=$(dbus-send --print-reply \
+ --type=method_call \
+ --system \
+ --dest=org.freedesktop.login1 \
+ /org/freedesktop/login1 \
+ org.freedesktop.DBus.Properties.Get \
+ string:org.freedesktop.login1.Manager string:ScheduledShutdown)
+ if ! echo $output | grep 'string "reboot"'; then
+ echo "Failed to detect scheduled reboot in logind output:"
+ echo "$output"
+ exit 1
+ fi
+
+ REBOOT
--- /dev/null
+summary: Ensure that the writable paths on the image are correct
+systems: [ubuntu-core-16-64, ubuntu-core-16-arm-64, ubuntu-core-16-arm-32]
+execute: |
+ echo "Ensure everything in writable-paths is actually writable"
+ cat /etc/system-image/writable-paths | while read -r line; do
+ line=$(echo $line | sed -e '/\s*#.*$/d')
+ if [ -z "$line" ]; then
+ continue;
+ fi
+ # a writable-path may be either a file or a directory
+ dir_or_file=$(echo $line|cut -f1 -d' ')
+ if [ ! -e "$dir_or_file" ]; then
+ echo "$dir_or_file" >> missing
+ elif [ -f "$dir_or_file" ]; then
+ if ! touch "$dir_or_file"; then
+ echo "$dir_or_file" >> broken
+ fi
+ elif ! touch "$dir_or_file"/random-name-that-I-made-up; then
+ echo "$dir_or_file" >> broken
+ fi
+ rm -f $dir_or_file/random-name-that-I-made-up
+ done
+
+ if [ -s "broken" ]; then
+ echo "The following writable paths are not writable:"
+ cat broken
+ fi
+ if [ -s "missing" ]; then
+ echo "The following writable paths are missing:"
+ cat missing
+ fi
+ # FIMXE: make missing fatal as well
+ #if [ -s missing ] || [ -s broken ]; then
+ # exit 1
+ #fi
+ if [ -s broken ]; then
+ exit 1
+ fi
--- /dev/null
+summary: Check that a unity snap can start and its window is shown
+
+environment:
+ DISPLAY: ":99.0"
+
+systems: [ubuntu-16.04-64]
+
+execute:
+ # FIXME Test is broken. Downloads HUNDREDS and HUNDREDS of packages,
+ # and asks for input on configuration file diffs.
+
+disabled_prepare: |
+ apt install -y x11-utils xvfb unity
+
+disabled_restore: |
+ systemctl stop unity-app
+ apt remove -y x11-utils xvfb unity
+ apt autoremove -y
+
+disabled_execute: |
+ echo "Given a unity snap is installed"
+ snap install ubuntu-clock-app
+
+ echo "When the app is started"
+ systemd-run --unit unity-app --setenv=DISPLAY=$DISPLAY --uid $(id -u test) $(which xvfb-run) --server-args="$DISPLAY -screen 0 1200x960x24 -ac +extension RANDR" $(which ubuntu-clock-app.clock)
+
+ echo "Then the app window is created"
+ expected=".*?\"qmlscene: clockMainView\": \(\"qmlscene\" \"com\.ubuntu\.clock\"\)"
+ while ! xwininfo -tree -root | grep -Pq "$expected"; do sleep 1; done
--- /dev/null
+summary: Check that snap apps and services can write to writable areas.
+
+environment:
+ # Ensure that running purely from the deb (without re-exec) works
+ # correctly
+ SNAP_REEXEC/reexec0: 0
+ SNAP_REEXEC/reexec1: 1
+
+prepare: |
+ snapbuild $TESTSLIB/snaps/data-writer .
+
+restore: |
+ rm data-writer_1.0_all.snap
+
+execute: |
+ snap install --dangerous data-writer_1.0_all.snap
+
+ echo "Apps can write to writable areas"
+ data-writer.app
+ [ -f /var/snap/data-writer/x1/from-app ]
+ [ -f /var/snap/data-writer/common/from-app ]
+ [ -f /root/snap/data-writer/x1/from-app ]
+ # TODO: As soon as `snap run` is used (which creates this directory),
+ # uncomment the following line:
+ #[ -f /root/snap/data-writer/common/from-app ]
+
+ echo "Waiting for data writer service to finish..."
+ while [ ! -f /root/snap/data-writer/x1/from-service ]; do
+ sleep 1
+ done
+
+ echo "Services can write to writable areas"
+ [ -f /var/snap/data-writer/x1/from-service ]
+ [ -f /var/snap/data-writer/common/from-service ]
+ # TODO: As soon as `snap run` is used (which creates this directory),
+ # uncomment the following line:
+ #[ -f /root/snap/data-writer/common/from-service ]
--- /dev/null
+# Test gadget snap with pre-installed snaps
+
+1. Branch snappy-systems
+2. Modify the `snap.yaml` to add a snap, e.g.:
+
+ ```diff
+ === modified file 'generic-amd64/meta/snap.yaml'
+ --- generic-amd64/meta/snap.yaml 2015-07-03 12:50:03 +0000
+ +++ generic-amd64/meta/snap.yaml 2015-11-09 16:26:12 +0000
+ @@ -7,6 +7,8 @@
+ config:
+ ubuntu-core:
+ autopilot: true
+ + config-example-bash:
+ + msg: "huzzah\n"
+
+ gadget:
+ branding:
+ @@ -20,3 +22,7 @@
+ boot-assets:
+ files:
+ - path: grub.cfg
+ +
+ + software:
+ + built-in:
+ + - config-example-bash.canonical
+ ```
+
+ (for amd64, or modify for other arch).
+
+3. Build the gadget snap.
+4. Create an image using the gadget snap.
+5. Boot the image
+6. Run:
+
+ sudo journalctl -u snapd.firstboot.service
+
+ * Check that it shows no errors.
+
+
+7. Run:
+
+ config-example-bash.hello
+
+ * Check that it prints `huzzah`.
+
+# Test gadget snap with modules
+
+1. Branch snappy-systems
+2. Modify the `snap.yaml` to add a module, e.g.:
+
+ ```diff
+ === modified file 'generic-amd64/meta/snap.yaml'
+ --- generic-amd64/meta/snap.yaml 2015-07-03 12:50:03 +0000
+ +++ generic-amd64/meta/snap.yaml 2015-11-12 10:14:30 +0000
+ @@ -7,6 +7,7 @@
+ config:
+ ubuntu-core:
+ autopilot: true
+ + load-kernel-modules: [tea]
+
+ gadget:
+ branding:
+
+ ```
+
+3. Build the gadget snap.
+4. Create an image using the gadget snap.
+5. Boot the image.
+6. Run:
+
+ sudo journalctl -u snapd.firstboot.service
+
+ * Check that it shows no errors.
+
+
+7. Check that the output of `lsmod` includes the module you requested. With the above example,
+
+ lsmod | grep tea
+
+# Test resize of writable partition
+
+1. Get the start of the *writable* partition:
+
+ parted /path/to/ubuntu-snappy.img unit b print
+
+ * Note down the number of bytes in the *Start* column for the *writable* partition.
+
+2. Make a loopback block device for the writable partition, replacing *{start}* with the number
+ from the previous step:
+
+ sudo losetup -f --show -o {start} /path/to/ubuntu-snappy.img
+
+ * Note down the loop device.
+
+3. Shrink the file system to the minimum, replacing *{dev}* with the device from the previous
+ step:
+
+ sudo e2fsck -f {dev}
+ sudo resize2fs -M {dev}
+
+4. Delete the loopback block device:
+
+ sudo losetup -d {dev}
+
+5. Get the end of the *writable* partition:
+
+ parted /path/to/ubuntu-snappy.img unit b print
+
+ * Note down the *Number* of the *writable* partition and the number of bytes in the *End*
+ column.
+
+6. Resize the *writable* partition, using the partition *{number}* from the last step, and
+ replacing the *{end}* with a value that leaves more than 10% space free at the end.
+
+ parted /path/to/ubuntu-snappy.img unit b resizepart {number} {end*85%}
+
+7. Boot the image.
+
+8. Print the free space of the file system, replacing *{dev}* with the device that has the
+ *writable* partition:
+
+ sudo parted -s {dev} unit % print free
+
+ * Check that the writable partition was resized to occupy all the empty space.
+
+# Test Mir interface by running Mir kiosk snap examples
+
+1. Install Virtual Machine Manager
+2. Stitch together a new image
+3. Build both the mir-server and the mir-client snaps from lp:~mir-team/+junk/mir-server-snap and lp:~mir-team/+junk/snapcraft-mir-client
+4. Copy over the snaps and sideload install the mir-server snap, which should result in a mir-server launching black blank screen with a mouse available.
+5. Now install the mir-client snap.
+6. Manually connect mir-client:mir to mir-server:mir due to bug 1577897, then start the mir-client service manually.
+7. This should result in the Qt clock example app being displayed.
+
+# Test serial-port interface using miniterm app
+
+1. Using Ubuntu classic build and install a simple snap containing the Python
+ pySerial module. Define a app that runs the module and starts miniterm.
+
+```yaml
+ name: miniterm
+ version: 1
+ summary: pySerial miniterm in a snap
+ description: |
+ Simple snap that contains the modules necessary to run
+ pySerial. Useful for testing serial ports.
+ confinement: strict
+ apps:
+ open:
+ command: python3 -m serial.tools.miniterm
+ plugs: [serial-port]
+ parts:
+ my-part:
+ plugin: nil
+ stage-packages:
+ - python3-serial
+```
+
+2. Ensure the 'serial-port' interface is connected to miniterm
+3. Use sudo miniterm.open /dev/tty<DEV> to open a serial port
+
+# Test pulseaudio interface using paplay, pactl
+
+1. Using a Snappy core image on a device like an RPi2/3, install the
+ build and install the simple-pulseaudio snap from the following
+ git repo:
+ git://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/examples
+2. $ cd examples/simple-pulseaudio
+3. Ensure that the 'pulseaudio' interface is connected to paplay
+ $ sudo snap interfaces
+4. Use /snap/bin/simple-pulseaudio.pactl stat and verify that you see
+ valid output status from pulseaudio
+5. Use /snap/bin/simple-pulseaudio.paplay $SNAP/usr/share/sounds/alsa/Noise.wav and verify
+ that you can hear the sound playing
+
+# Test bluetooth-control interface
+
+1. Using Ubuntu classic build and install the bluetooth-tests snap
+ from the store.
+
+2. Stop system BlueZ service
+
+$ sudo systemctl stop bluetooth
+
+or if you have the bluez snap installed
+
+$ snap remove bluez
+
+3. Run one of the tests provided by the bluetooth-tests snap
+
+ $ sudo /snap/bin/bluetooth-tests.hci-tester
+
+ and verify it actually passes. If some of the tests fail
+ there will be a problem with the particular kernel used on
+ the device.
+
+# Test tpm interface with tpm-tools
+
+1. Install tpm snap from store.
+2. Connect plug tpm:tpm to slot ubuntu-core:tpm.
+3. Reboot the system so daemon in tpm snap can get proper permissions.
+4. Use tpm.version to read from tpm device and make sure it shows no error.
+
+ $ tpm.version
+ xKV TPM 1.2 Version Info:
+ Chip Version: 1.2.5.81
+ Spec Level: 2
+ Errata Revision: 3
+ TPM Vendor ID: WEC
+ Vendor Specific data: 0000
+ TPM Version: 01010000
+ Manufacturer Info: 57454300
+
+# Test fwupd interface with uefi-fw-tools
+
+1. Ensure your BIOS support UEFI firmware upgrading via UEFI capsule format
+2. Install the uefi-fw-tools snap from the store
+3. Ensure the 'fwupd' interface is connected
+
+ $ sudo snap connect uefi-fw-tools:fwupdmgr uefi-fw-tools:fwupd
+
+4. Check if the device support UEFI firmware updates
+
+ $ sudo uefi-fw-tools.fwupdmgr get-devices
+
+5. Get available UEFI firmware from the server
+
+ $ sudo uefi-fw-tools.fwupdmgr refresh
+
+6. Download firmware
+
+ $ sudo uefi-fw-tools.fwupdmgr update
+
+7. Reboot and ensure it start the upgrading process
--- /dev/null
+summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1580018
+details: |
+ On classic systems, the /etc/alternatives directory, if one exists, should
+ be coming from the core snap (or the ubuntu-core snap). This allows snaps
+ to rely on deterministic behavior, isolated from any changes imposed by
+ Debian's alternatives system.
+# This test only applies to classic systems
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-arm-64]
+prepare: |
+ echo "Having installed the test snap"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+execute: |
+ echo "We can check the inode number of /etc/alternatives"
+ host_inode="$(stat -c '%i' /etc/alternatives)"
+ if [ -e /snap/core/current ]; then
+ core_inode="$(stat -c '%i' /snap/core/current/etc/alternatives)"
+ else
+ core_inode="$(stat -c '%i' /snap/ubuntu-core/current/etc/alternatives)"
+ fi
+ effective_inode="$(test-snapd-tools.cmd stat -c '%i' /etc/alternatives)"
+ echo "The inode number as seen from a confined snap should be that of the /etc/alternatives from the core snap"
+ [ "$host_inode" != "$core_inode" ]
+ [ "$effective_inode" = "$core_inode" ]
--- /dev/null
+summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1595444
+details: |
+ This task checks the behavior of snap-confine when it is started from
+ a directory that doesn't exist in the execution environment (chroot).
+# This test only applies to classic systems
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-arm-64]
+prepare: |
+ echo "Having installed the test snap"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ echo "Hanving created a directory not present in the core snap"
+ mkdir -p "/foo"
+execute: |
+ echo "We can go to a location that is available in all snaps (/tmp)"
+ echo "We can run the 'pwd' tool and it reports /tmp"
+ [ "$(cd /tmp && test-snapd-tools.cmd pwd)" = "/tmp" ]
+ echo "But if we go to a location that is not available to snaps (e.g. /foo)"
+ echo "Then snap-confine moves us to /var/lib/snapd/void"
+ [ "$(cd /foo && test-snapd-tools.cmd pwd)" = "/var/lib/snapd/void" ]
+ echo "And that directory is not readable or writable"
+ [ "$(cd /foo && test-snapd-tools.cmd ls 2>&1)" = "ls: cannot open directory '.': Permission denied" ];
+restore: |
+ rm -f -d /foo
+ # NOTE: the snap is blocked by apparmor from reading /var/lib/snapd/void
+ dmesg -c
--- /dev/null
+summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1597839
+# This test only applies to classic systems
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-arm-64]
+details: |
+ The snappy execution environment should contain the /lib/modules directory
+ from the host filesystem when running on a classic distribution
+prepare: |
+ echo "Having installed the test snap"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+execute: |
+ echo "We can ensure that the /lib/modules/$(uname -r) directory exists"
+ test-snapd-tools.cmd test -d /lib/modules/$(uname -r)
--- /dev/null
+summary: Regression test for https://bugs.launchpad.net/snap-confine/+bug/1597842
+details: |
+ The snap execution environment is expected to contain the /usr/src
+ directory from the classic distribution. Certain snaps may use that to
+ access kernel sources.
+# This test only applies to classic systems
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-arm-64]
+prepare: |
+ echo "Having installed the test snap"
+ . $TESTSLIB/snaps.sh
+ # NOTE: devmode is required because there's no interface for reading /usr/src/.canary
+ install_local_devmode test-snapd-tools
+ echo "Having prepared a canary file in /usr/src/.canary"
+ mv /usr/src /usr/src.orig || true
+ mkdir -p /usr/src
+ echo canary > /usr/src/.canary
+execute: |
+ echo "The canary file in /usr/src can be read"
+ [ "$(test-snapd-tools.cmd cat /usr/src/.canary)" = "canary" ]
+restore: |
+ rm -f /usr/src/.canary
+ rm -f -d /usr/src
+ mv /usr/src.orig /usr/src || true
--- /dev/null
+summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1599891
+execute: |
+ snap_confine=/usr/lib/snapd/snap-confine
+ echo "Seeing that snap-confine is in $snap_confine"
+
+ echo "I also see a corresponding apparmor profile"
+ cat "/sys/kernel/security/apparmor/profiles" | MATCH "$snap_confine \(enforce\)"
--- /dev/null
+summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1606277
+details: |
+ A missing bind mount for /var/log prevents access to system log files
+ even if the log-observe interface is being used.
+prepare: |
+ . "$TESTSLIB/snaps.sh"
+ echo "Having installed a test snap"
+ install_local log-observe-consumer
+ echo "And having connected the log-observe interface"
+ snap connect log-observe-consumer:log-observe :log-observe
+execute: |
+ echo "We can now see a non-empty /var/log directory"
+ [ "$(log-observe-consumer.cmd ls /var/log | wc -l)" != 0 ]
--- /dev/null
+summary: Check that /root is bind mounted to the real /root
+prepare: |
+ echo "Having installed a test snap in devmode"
+ . $TESTSLIB/snaps.sh
+ install_local_devmode test-snapd-tools
+ echo "Having added a canary file in /root"
+ echo "test" > /root/canary
+execute: |
+ echo "We can see the canary file in /root"
+ [ "$(test-snapd-tools.cmd cat /root/canary)" = "test" ]
+restore: |
+ rm -f /root/canary
--- /dev/null
+summary: Check that /var/lib/lxd is bind mounted to the real thing if one exists
+details: |
+ After switching to the chroot-based snap-confine the LXD snap stopped
+ working (even in devmode) because it relied on access to /var/lib/lxd from
+ the host filesystem. While this would never work in an all-snap image it is
+ still important to ensure that it works in classic devmode environment.
+# This test only applies to classic systems
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-32, -ubuntu-core-16-arm-64]
+prepare: |
+ echo "Having installed the test snap in devmode"
+ . $TESTSLIB/snaps.sh
+ install_local_devmode test-snapd-tools
+ echo "Having created a canary file in /var/lib/lxd"
+ mkdir -p /var/lib/lxd
+ echo "test" > /var/lib/lxd/canary
+execute: |
+ echo "We can see the canary file in /var/lib/lxd"
+ [ "$(test-snapd-tools.cmd cat /var/lib/lxd/canary)" = "test" ]
+restore: |
+ rm -f /var/lib/lxd/canary
+ # When the host already has LXD installed we cannot just remove this
+ rmdir /var/lib/lxd || :
--- /dev/null
+summary: Check that entire snap can be shared with the content interface
+prepare: |
+ echo "Having installed a pair of snaps that share content"
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-content-slot
+ install_local test-snapd-content-plug
+ echo "We can connect them together"
+ snap connect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot
+execute: |
+ echo "We can now see that the content is shared"
+ test-snapd-content-plug.content-plug | grep "Some shared content"
+ echo "And fstab files are created"
+ [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ]
--- /dev/null
+summary: Check that user namespace can be unshared within snap apps
+details: |
+ Snap-confine used to "leak" the root filesystem directory across the
+ pivot_root call. This caused checks in the kernel to fail and resulted in
+ the inability to create user namespaces from sufficiently privileged or
+ devmode snaps.
+prepare: |
+ echo "Having installed a test snap in devmode"
+ . $TESTSLIB/snaps.sh
+ install_local_devmode test-snapd-tools
+execute: |
+ echo "We can run unshare -U as a regular user and expect it to work"
+ test-snapd-tools.cmd unshare -U true
--- /dev/null
+summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1630479
+details: |
+ The PATH environment variable needs to make sense for the layout of the
+ core snap. Snap-confine contains code that sets it accordingly but during
+ the introduction of the namespace sharing feature that code would run only
+ when the namespace was initially constructed. Subsequent calls would not
+ see the correct path.
+prepare: |
+ echo "Having installed a test snap"
+ . $TESTSLIB/snaps.sh
+ install_local_devmode test-snapd-tools
+execute: |
+ echo "We can observe the value of PATH twice"
+ revision=$(readlink /snap/test-snapd-tools/current)
+ one="$(test-snapd-tools.cmd sh -c 'echo $PATH')"
+ two="$(test-snapd-tools.cmd sh -c 'echo $PATH')"
+ echo "We can see that PATH is stable across calls"
+ [ "$one" = "$two" ]
+ echo "We can make sure that it has the right value"
+ [ "$one" = "/usr/sbin:/usr/bin:/sbin:/bin:/usr/games" ]
+ # NOTE: the following parts are notably absent from PATH
+ # - /snap/test-snapd-tools/$revision/bin
+ # - /snap/test-snapd-tools/$revision/usr/bin
+ #
+ # This is because our test snap is not built with snapcraft and those path
+ # elements are only added by the wrappers generated by snapcraft.
--- /dev/null
+summary: Check that upgrade works
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-device-amd64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+restore: |
+ rm -f /var/tmp/myevil.txt
+execute: |
+ . "$TESTSLIB/apt.sh"
+
+ echo Install previous version...
+ dpkg -l snapd snap-confine ubuntu-core-launcher || true
+ apt-get install -y snapd
+
+ prevsnapdver=$(snap --version|grep "snapd ")
+
+ echo Install a snap with it
+ snap install test-snapd-tools
+
+ echo Sanity check install
+ test-snapd-tools.echo Hello | grep Hello
+ test-snapd-tools.env | grep SNAP_NAME=test-snapd-tools
+
+ echo Do upgrade
+ # allow-downgrades prevents errors when new versions hit the archive, for instance,
+ # trying to install 2.11ubuntu1 over 2.11+0.16.04
+ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then
+ # apt_install_local uses dpkg directly which happily downgrades
+ apt_install_local ${GOPATH}/snap-confine*.deb ${GOPATH}/ubuntu-core-launcher*.deb ${GOPATH}/snapd*.deb
+ else
+ # not using apt_install_local here as this will not have
+ # --allow-downgrades
+ apt install -y --allow-downgrades ${GOPATH}/snapd*.deb ${GOPATH}/snap-confine*.deb ${GOPATH}/ubuntu-core-launcher*.deb
+ fi
+
+ snapdver=$(snap --version|grep "snapd ")
+ [ "$snapdver" != "$prevsnapdver" ]
+
+ echo Sanity check already installed snaps after upgrade
+ snap list | grep core
+ snap list | grep test-snapd-tools
+ test-snapd-tools.echo Hello | grep Hello
+ test-snapd-tools.env | grep SNAP_NAME=test-snapd-tools
+ echo Hello > /var/tmp/myevil.txt
+ test-snapd-tools.cat /var/tmp/myevil.txt && exit 1 || true
+
+ . "$TESTSLIB/names.sh"
+
+ echo Check migrating to types in state
+ coreType=$(jq -r '.data.snaps["'${core_name}'"].type' /var/lib/snapd/state.json)
+ testSnapType=$(jq -r '.data.snaps["test-snapd-tools"].type' /var/lib/snapd/state.json)
+ [ "$coreType" = "os" ]
+ [ "$testSnapType" = "app" ]
--- /dev/null
+#!/bin/bash
+
+BACKEND="${1:-linode:}"
+ITERATIONS=${2:-10}
+OUTPUT_FILE="${3:-${PWD}/benchmark.out}"
+SUCCESSFUL_EXECUTIONS=0
+
+rm -f "$OUTPUT_FILE"
+
+for i in $(seq "$ITERATIONS"); do
+ echo "Running iteration $i of $ITERATIONS"
+ START_TIME=$SECONDS
+ spread -v "$BACKEND"
+ if [ "$?" = "0" ]; then
+ SUCCESSFUL_EXECUTIONS=$((SUCCESSFUL_EXECUTIONS + 1))
+ ITERATION_TIME=$((SECONDS - START_TIME))
+ TOTAL_TIME=$((TOTAL_TIME + ITERATION_TIME))
+ echo "$ITERATION_TIME" >> "$OUTPUT_FILE"
+ fi
+done
+echo "$SUCCESSFUL_EXECUTIONS successful executions out of $ITERATIONS" >> "$OUTPUT_FILE"
+echo "Average: $(echo "scale=2; $TOTAL_TIME / $SUCCESSFUL_EXECUTIONS" | bc)s" >> "$OUTPUT_FILE"
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package testutil
+
+import (
+ "gopkg.in/check.v1"
+)
+
+// BaseTest is a structure used as a base test suite for all the snappy
+// tests.
+type BaseTest struct {
+ cleanupHandlers []func()
+}
+
+// SetUpTest prepares the cleanup
+func (s *BaseTest) SetUpTest(c *check.C) {
+ s.cleanupHandlers = nil
+}
+
+// TearDownTest cleans up the channel.ini files in case they were changed by
+// the test.
+// It also runs the cleanup handlers
+func (s *BaseTest) TearDownTest(c *check.C) {
+ // run cleanup handlers and clear the slice
+ for _, f := range s.cleanupHandlers {
+ f()
+ }
+ s.cleanupHandlers = nil
+}
+
+// AddCleanup adds a new cleanup function to the test
+func (s *BaseTest) AddCleanup(f func()) {
+ s.cleanupHandlers = append(s.cleanupHandlers, f)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package testutil
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+
+ "gopkg.in/check.v1"
+)
+
+type containsChecker struct {
+ *check.CheckerInfo
+}
+
+// Contains is a Checker that looks for a elem in a container.
+// The elem can be any object. The container can be an array, slice or string.
+var Contains check.Checker = &containsChecker{
+ &check.CheckerInfo{Name: "Contains", Params: []string{"container", "elem"}},
+}
+
+func commonEquals(container, elem interface{}, result *bool, error *string) bool {
+ containerV := reflect.ValueOf(container)
+ elemV := reflect.ValueOf(elem)
+ switch containerV.Kind() {
+ case reflect.Slice, reflect.Array, reflect.Map:
+ containerElemType := containerV.Type().Elem()
+ if containerElemType.Kind() == reflect.Interface {
+ // Ensure that element implements the type of elements stored in the container.
+ if !elemV.Type().Implements(containerElemType) {
+ *result = false
+ *error = fmt.Sprintf(""+
+ "container has items of interface type %s but expected"+
+ " element does not implement it", containerElemType)
+ return true
+ }
+ } else {
+ // Ensure that type of elements in container is compatible with elem
+ if containerElemType != elemV.Type() {
+ *result = false
+ *error = fmt.Sprintf(
+ "container has items of type %s but expected element is a %s",
+ containerElemType, elemV.Type())
+ return true
+ }
+ }
+ case reflect.String:
+ // When container is a string, we expect elem to be a string as well
+ if elemV.Kind() != reflect.String {
+ *result = false
+ *error = fmt.Sprintf("element is a %T but expected a string", elem)
+ } else {
+ *result = strings.Contains(containerV.String(), elemV.String())
+ *error = ""
+ }
+ return true
+ }
+ return false
+}
+
+func (c *containsChecker) Check(params []interface{}, names []string) (result bool, error string) {
+ defer func() {
+ if v := recover(); v != nil {
+ result = false
+ error = fmt.Sprint(v)
+ }
+ }()
+ var container interface{} = params[0]
+ var elem interface{} = params[1]
+ if commonEquals(container, elem, &result, &error) {
+ return
+ }
+ // Do the actual test using ==
+ switch containerV := reflect.ValueOf(container); containerV.Kind() {
+ case reflect.Slice, reflect.Array:
+ for length, i := containerV.Len(), 0; i < length; i++ {
+ itemV := containerV.Index(i)
+ if itemV.Interface() == elem {
+ return true, ""
+ }
+ }
+ return false, ""
+ case reflect.Map:
+ for _, keyV := range containerV.MapKeys() {
+ itemV := containerV.MapIndex(keyV)
+ if itemV.Interface() == elem {
+ return true, ""
+ }
+ }
+ return false, ""
+ default:
+ return false, fmt.Sprintf("%T is not a supported container", container)
+ }
+}
+
+type deepContainsChecker struct {
+ *check.CheckerInfo
+}
+
+// DeepContains is a Checker that looks for a elem in a container using
+// DeepEqual. The elem can be any object. The container can be an array, slice
+// or string.
+var DeepContains check.Checker = &deepContainsChecker{
+ &check.CheckerInfo{Name: "DeepContains", Params: []string{"container", "elem"}},
+}
+
+func (c *deepContainsChecker) Check(params []interface{}, names []string) (result bool, error string) {
+ var container interface{} = params[0]
+ var elem interface{} = params[1]
+ if commonEquals(container, elem, &result, &error) {
+ return
+ }
+ // Do the actual test using reflect.DeepEqual
+ switch containerV := reflect.ValueOf(container); containerV.Kind() {
+ case reflect.Slice, reflect.Array:
+ for length, i := containerV.Len(), 0; i < length; i++ {
+ itemV := containerV.Index(i)
+ if reflect.DeepEqual(itemV.Interface(), elem) {
+ return true, ""
+ }
+ }
+ return false, ""
+ case reflect.Map:
+ for _, keyV := range containerV.MapKeys() {
+ itemV := containerV.MapIndex(keyV)
+ if reflect.DeepEqual(itemV.Interface(), elem) {
+ return true, ""
+ }
+ }
+ return false, ""
+ default:
+ return false, fmt.Sprintf("%T is not a supported container", container)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// 20160229: The tests with gccgo on powerpc fail for this file
+// and it will loop endlessly. This is not reproducible
+// with gccgo on amd64. Given that it's a relatively little
+// used arch we disable the tests in here to workaround this
+// gccgo bug.
+// +build !ppc
+
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package testutil
+
+import (
+ "reflect"
+ "runtime"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) {
+ TestingT(t)
+}
+
+type CheckersS struct{}
+
+var _ = Suite(&CheckersS{})
+
+func testInfo(c *C, checker Checker, name string, paramNames []string) {
+ info := checker.Info()
+ if info.Name != name {
+ c.Fatalf("Got name %s, expected %s", info.Name, name)
+ }
+ if !reflect.DeepEqual(info.Params, paramNames) {
+ c.Fatalf("Got param names %#v, expected %#v", info.Params, paramNames)
+ }
+}
+
+func testCheck(c *C, checker Checker, result bool, error string, params ...interface{}) ([]interface{}, []string) {
+ info := checker.Info()
+ if len(params) != len(info.Params) {
+ c.Fatalf("unexpected param count in test; expected %d got %d", len(info.Params), len(params))
+ }
+ names := append([]string{}, info.Params...)
+ resultActual, errorActual := checker.Check(params, names)
+ if resultActual != result || errorActual != error {
+ c.Fatalf("%s.Check(%#v) returned (%#v, %#v) rather than (%#v, %#v)",
+ info.Name, params, resultActual, errorActual, result, error)
+ }
+ return params, names
+}
+
+func (s *CheckersS) TestUnsupportedTypes(c *C) {
+ testInfo(c, Contains, "Contains", []string{"container", "elem"})
+ testCheck(c, Contains, false, "int is not a supported container", 5, nil)
+ testCheck(c, Contains, false, "bool is not a supported container", false, nil)
+ testCheck(c, Contains, false, "element is a int but expected a string", "container", 1)
+}
+
+func (s *CheckersS) TestContainsVerifiesTypes(c *C) {
+ testInfo(c, Contains, "Contains", []string{"container", "elem"})
+ testCheck(c, Contains,
+ false, "container has items of type int but expected element is a string",
+ [...]int{1, 2, 3}, "foo")
+ testCheck(c, Contains,
+ false, "container has items of type int but expected element is a string",
+ []int{1, 2, 3}, "foo")
+ // This looks tricky, Contains looks at _values_, not at keys
+ testCheck(c, Contains,
+ false, "container has items of type int but expected element is a string",
+ map[string]int{"foo": 1, "bar": 2}, "foo")
+ testCheck(c, Contains,
+ false, "container has items of type int but expected element is a string",
+ map[string]int{"foo": 1, "bar": 2}, "foo")
+}
+
+type animal interface {
+ Sound() string
+}
+
+type dog struct{}
+
+func (d *dog) Sound() string {
+ return "bark"
+}
+
+type cat struct{}
+
+func (c *cat) Sound() string {
+ return "meow"
+}
+
+type tree struct{}
+
+func (s *CheckersS) TestContainsVerifiesInterfaceTypes(c *C) {
+ testCheck(c, Contains,
+ false, "container has items of interface type testutil.animal but expected element does not implement it",
+ [...]animal{&dog{}, &cat{}}, &tree{})
+ testCheck(c, Contains,
+ false, "container has items of interface type testutil.animal but expected element does not implement it",
+ []animal{&dog{}, &cat{}}, &tree{})
+ testCheck(c, Contains,
+ false, "container has items of interface type testutil.animal but expected element does not implement it",
+ map[string]animal{"dog": &dog{}, "cat": &cat{}}, &tree{})
+}
+
+func (s *CheckersS) TestContainsString(c *C) {
+ c.Assert("foo", Contains, "f")
+ c.Assert("foo", Contains, "fo")
+ c.Assert("foo", Not(Contains), "foobar")
+}
+
+type myString string
+
+func (s *CheckersS) TestContainsCustomString(c *C) {
+ c.Assert(myString("foo"), Contains, myString("f"))
+ c.Assert(myString("foo"), Contains, myString("fo"))
+ c.Assert(myString("foo"), Not(Contains), myString("foobar"))
+ c.Assert("foo", Contains, myString("f"))
+ c.Assert("foo", Contains, myString("fo"))
+ c.Assert("foo", Not(Contains), myString("foobar"))
+ c.Assert(myString("foo"), Contains, "f")
+ c.Assert(myString("foo"), Contains, "fo")
+ c.Assert(myString("foo"), Not(Contains), "foobar")
+}
+
+func (s *CheckersS) TestContainsArray(c *C) {
+ c.Assert([...]int{1, 2, 3}, Contains, 1)
+ c.Assert([...]int{1, 2, 3}, Contains, 2)
+ c.Assert([...]int{1, 2, 3}, Contains, 3)
+ c.Assert([...]int{1, 2, 3}, Not(Contains), 4)
+ c.Assert([...]animal{&dog{}, &cat{}}, Contains, &dog{})
+ c.Assert([...]animal{&cat{}}, Not(Contains), &dog{})
+}
+
+func (s *CheckersS) TestContainsSlice(c *C) {
+ c.Assert([]int{1, 2, 3}, Contains, 1)
+ c.Assert([]int{1, 2, 3}, Contains, 2)
+ c.Assert([]int{1, 2, 3}, Contains, 3)
+ c.Assert([]int{1, 2, 3}, Not(Contains), 4)
+ c.Assert([]animal{&dog{}, &cat{}}, Contains, &dog{})
+ c.Assert([]animal{&cat{}}, Not(Contains), &dog{})
+}
+
+func (s *CheckersS) TestContainsMap(c *C) {
+ c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 1)
+ c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 2)
+ c.Assert(map[string]int{"foo": 1, "bar": 2}, Not(Contains), 3)
+ c.Assert(map[string]animal{"dog": &dog{}, "cat": &cat{}}, Contains, &dog{})
+ c.Assert(map[string]animal{"cat": &cat{}}, Not(Contains), &dog{})
+}
+
+// Arbitrary type that is not comparable
+type myStruct struct {
+ attrs map[string]string
+}
+
+func (s *CheckersS) TestContainsUncomparableType(c *C) {
+ if runtime.Compiler != "go" {
+ c.Skip("this test only works on go (not gccgo)")
+ }
+
+ elem := myStruct{map[string]string{"k": "v"}}
+ containerArray := [...]myStruct{elem}
+ containerSlice := []myStruct{elem}
+ containerMap := map[string]myStruct{"foo": elem}
+ errMsg := "runtime error: comparing uncomparable type testutil.myStruct"
+ testInfo(c, Contains, "Contains", []string{"container", "elem"})
+ testCheck(c, Contains, false, errMsg, containerArray, elem)
+ testCheck(c, Contains, false, errMsg, containerSlice, elem)
+ testCheck(c, Contains, false, errMsg, containerMap, elem)
+}
+
+func (s *CheckersS) TestDeepContainsUnsupportedTypes(c *C) {
+ testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"})
+ testCheck(c, DeepContains, false, "int is not a supported container", 5, nil)
+ testCheck(c, DeepContains, false, "bool is not a supported container", false, nil)
+ testCheck(c, DeepContains, false, "element is a int but expected a string", "container", 1)
+}
+
+func (s *CheckersS) TestDeepContainsVerifiesTypes(c *C) {
+ testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"})
+ testCheck(c, DeepContains,
+ false, "container has items of type int but expected element is a string",
+ [...]int{1, 2, 3}, "foo")
+ testCheck(c, DeepContains,
+ false, "container has items of type int but expected element is a string",
+ []int{1, 2, 3}, "foo")
+ // This looks tricky, DeepContains looks at _values_, not at keys
+ testCheck(c, DeepContains,
+ false, "container has items of type int but expected element is a string",
+ map[string]int{"foo": 1, "bar": 2}, "foo")
+}
+
+func (s *CheckersS) TestDeepContainsString(c *C) {
+ c.Assert("foo", DeepContains, "f")
+ c.Assert("foo", DeepContains, "fo")
+ c.Assert("foo", Not(DeepContains), "foobar")
+}
+
+func (s *CheckersS) TestDeepContainsCustomString(c *C) {
+ c.Assert(myString("foo"), DeepContains, myString("f"))
+ c.Assert(myString("foo"), DeepContains, myString("fo"))
+ c.Assert(myString("foo"), Not(DeepContains), myString("foobar"))
+ c.Assert("foo", DeepContains, myString("f"))
+ c.Assert("foo", DeepContains, myString("fo"))
+ c.Assert("foo", Not(DeepContains), myString("foobar"))
+ c.Assert(myString("foo"), DeepContains, "f")
+ c.Assert(myString("foo"), DeepContains, "fo")
+ c.Assert(myString("foo"), Not(DeepContains), "foobar")
+}
+
+func (s *CheckersS) TestDeepContainsArray(c *C) {
+ c.Assert([...]int{1, 2, 3}, DeepContains, 1)
+ c.Assert([...]int{1, 2, 3}, DeepContains, 2)
+ c.Assert([...]int{1, 2, 3}, DeepContains, 3)
+ c.Assert([...]int{1, 2, 3}, Not(DeepContains), 4)
+}
+
+func (s *CheckersS) TestDeepContainsSlice(c *C) {
+ c.Assert([]int{1, 2, 3}, DeepContains, 1)
+ c.Assert([]int{1, 2, 3}, DeepContains, 2)
+ c.Assert([]int{1, 2, 3}, DeepContains, 3)
+ c.Assert([]int{1, 2, 3}, Not(DeepContains), 4)
+}
+
+func (s *CheckersS) TestDeepContainsMap(c *C) {
+ c.Assert(map[string]int{"foo": 1, "bar": 2}, DeepContains, 1)
+ c.Assert(map[string]int{"foo": 1, "bar": 2}, DeepContains, 2)
+ c.Assert(map[string]int{"foo": 1, "bar": 2}, Not(DeepContains), 3)
+}
+
+func (s *CheckersS) TestDeepContainsUncomparableType(c *C) {
+ elem := myStruct{map[string]string{"k": "v"}}
+ containerArray := [...]myStruct{elem}
+ containerSlice := []myStruct{elem}
+ containerMap := map[string]myStruct{"foo": elem}
+ testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"})
+ testCheck(c, DeepContains, true, "", containerArray, elem)
+ testCheck(c, DeepContains, true, "", containerSlice, elem)
+ testCheck(c, DeepContains, true, "", containerMap, elem)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package testutil
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+
+ "gopkg.in/check.v1"
+)
+
+// MockCmd allows mocking commands for testing.
+type MockCmd struct {
+ binDir string
+ exeFile string
+ logFile string
+}
+
+var scriptTpl = `#!/bin/sh
+echo "$(basename "$0")" >> %[1]q
+for arg in "$@"; do
+ echo "$arg" >> %[1]q
+done
+echo >> %[1]q
+%s
+`
+
+// MockCommand adds a mocked command to PATH.
+//
+// The command logs all invocations to a dedicated log file. If script is
+// non-empty then it is used as is and the caller is responsible for how the
+// script behaves (exit code and any extra behavior). If script is empty then
+// the command exits successfully without any other side-effect.
+func MockCommand(c *check.C, basename, script string) *MockCmd {
+ binDir := c.MkDir()
+ exeFile := path.Join(binDir, basename)
+ logFile := path.Join(binDir, basename+".log")
+ err := ioutil.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, logFile, script)), 0700)
+ if err != nil {
+ panic(err)
+ }
+ os.Setenv("PATH", binDir+":"+os.Getenv("PATH"))
+ return &MockCmd{binDir: binDir, exeFile: exeFile, logFile: logFile}
+}
+
+// Also mock this command, using the same bindir and log
+// Useful when you want to check the ordering of things.
+func (cmd *MockCmd) Also(basename, script string) *MockCmd {
+ exeFile := path.Join(cmd.binDir, basename)
+ err := ioutil.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, cmd.logFile, script)), 0700)
+ if err != nil {
+ panic(err)
+ }
+ return &MockCmd{binDir: cmd.binDir, exeFile: exeFile, logFile: cmd.logFile}
+}
+
+// Restore removes the mocked command from PATH
+func (cmd *MockCmd) Restore() {
+ entries := strings.Split(os.Getenv("PATH"), ":")
+ for i, entry := range entries {
+ if entry == cmd.binDir {
+ entries = append(entries[:i], entries[i+1:]...)
+ break
+ }
+ }
+ os.Setenv("PATH", strings.Join(entries, ":"))
+}
+
+// Calls returns a list of calls that were made to the mock command.
+// of the form:
+// [][]string{
+// {"cmd", "arg1", "arg2"}, // first invocation of "cmd"
+// {"cmd", "arg1", "arg2"}, // second invocation of "cmd"
+// }
+func (cmd *MockCmd) Calls() [][]string {
+ raw, err := ioutil.ReadFile(cmd.logFile)
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ panic(err)
+ }
+ logContent := strings.TrimSuffix(string(raw), "\n")
+
+ allCalls := [][]string{}
+ calls := strings.Split(logContent, "\n\n")
+ for _, call := range calls {
+ call = strings.TrimSuffix(call, "\n")
+ allCalls = append(allCalls, strings.Split(call, "\n"))
+ }
+ return allCalls
+}
+
+// ForgetCalls purges the list of calls made so far
+func (cmd *MockCmd) ForgetCalls() {
+ err := os.Remove(cmd.logFile)
+ if os.IsNotExist(err) {
+ return
+ }
+ if err != nil {
+ panic(err)
+ }
+}
+
+// BinDir returns the location of the directory holding overridden commands.
+func (cmd *MockCmd) BinDir() string {
+ return cmd.binDir
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package testutil
+
+import (
+ "os/exec"
+
+ . "gopkg.in/check.v1"
+)
+
+type mockCommandSuite struct{}
+
+var _ = Suite(&mockCommandSuite{})
+
+func (s *mockCommandSuite) TestMockCommand(c *C) {
+ mock := MockCommand(c, "cmd", "true")
+ err := exec.Command("cmd", "first-run", "--arg1", "arg2", "a space").Run()
+ c.Assert(err, IsNil)
+ err = exec.Command("cmd", "second-run", "--arg1", "arg2", "a %s").Run()
+ c.Assert(err, IsNil)
+ c.Assert(mock.Calls(), DeepEquals, [][]string{
+ {"cmd", "first-run", "--arg1", "arg2", "a space"},
+ {"cmd", "second-run", "--arg1", "arg2", "a %s"},
+ })
+}
+
+func (s *mockCommandSuite) TestMockCommandAlso(c *C) {
+ mock := MockCommand(c, "fst", "")
+ also := mock.Also("snd", "")
+ defer mock.Restore()
+
+ c.Assert(exec.Command("fst").Run(), IsNil)
+ c.Assert(exec.Command("snd").Run(), IsNil)
+ c.Check(mock.Calls(), DeepEquals, [][]string{{"fst"}, {"snd"}})
+ c.Check(mock.Calls(), DeepEquals, also.Calls())
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package timeout
+
+import (
+ "encoding/json"
+ "time"
+)
+
+// Timeout is a time.Duration that knows how to roundtrip to json and yaml
+type Timeout time.Duration
+
+// DefaultTimeout specifies the timeout for services that do not specify StopTimeout
+var DefaultTimeout = Timeout(30 * time.Second)
+
+// MarshalJSON is from the json.Marshaler interface
+func (t Timeout) MarshalJSON() ([]byte, error) {
+ return json.Marshal(t.String())
+}
+
+// UnmarshalJSON is from the json.Unmarshaler interface
+func (t *Timeout) UnmarshalJSON(buf []byte) error {
+ var str string
+ if err := json.Unmarshal(buf, &str); err != nil {
+ return err
+ }
+
+ dur, err := time.ParseDuration(str)
+ if err != nil {
+ return err
+ }
+
+ *t = Timeout(dur)
+
+ return nil
+}
+
+func (t *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var str string
+ if err := unmarshal(&str); err != nil {
+ return err
+ }
+ dur, err := time.ParseDuration(str)
+ if err != nil {
+ return err
+ }
+ *t = Timeout(dur)
+ return nil
+}
+
+// String returns a string representing the duration
+func (t Timeout) String() string {
+ return time.Duration(t).String()
+}
+
+// Seconds returns the duration as a floating point number of seconds.
+func (t Timeout) Seconds() float64 {
+ return time.Duration(t).Seconds()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package timeout
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type TimeoutTestSuite struct {
+}
+
+var _ = Suite(&TimeoutTestSuite{})
+
+func (s *TimeoutTestSuite) TestTimeoutMarshal(c *C) {
+ bs, err := Timeout(DefaultTimeout).MarshalJSON()
+ c.Assert(err, IsNil)
+ c.Check(string(bs), Equals, `"30s"`)
+}
+
+type testT struct {
+ T Timeout `yaml:"T"`
+}
+
+func (s *TimeoutTestSuite) TestTimeoutMarshalIndirect(c *C) {
+ bs, err := json.Marshal(testT{DefaultTimeout})
+ c.Assert(err, IsNil)
+ c.Check(string(bs), Equals, `{"T":"30s"}`)
+}
+
+func (s *TimeoutTestSuite) TestTimeoutUnmarshalJSON(c *C) {
+ var t testT
+ c.Assert(json.Unmarshal([]byte(`{"T": "17ms"}`), &t), IsNil)
+ c.Check(t, DeepEquals, testT{T: Timeout(17 * time.Millisecond)})
+}
+
+func (s *TimeoutTestSuite) TestTimeoutUnmarshalYAML(c *C) {
+ var t testT
+ c.Assert(yaml.Unmarshal([]byte(`T: 17ms`), &t), IsNil)
+ c.Check(t, DeepEquals, testT{T: Timeout(17 * time.Millisecond)})
+}
--- /dev/null
+#!/bin/sh
+# -*- Mode: sh; indent-tabs-mode: t -*-
+
+set -e
+
+HERE="$(dirname $0)"
+
+OUTPUT="$HERE/po/snappy.pot"
+if [ -n "$1" ]; then
+ OUTPUT="$1"
+fi
+
+# ensure we have our xgettext-go
+go install github.com/snapcore/snapd/i18n/xgettext-go
+
+$GOPATH/bin/xgettext-go \
+ -o "$OUTPUT" \
+ --add-comments-tag=TRANSLATORS: \
+ --no-location \
+ --sort-output \
+ --package-name=snappy\
+ --msgid-bugs-address=snappy-devel@lists.ubuntu.com \
+ --keyword=i18n.G \
+ --keyword-plural=i18n.DG \
+ $(find $HERE -name "*.go" -type f)
+
+#xgettext -d snappy -o "$OUTPUT" --c++ --from-code=UTF-8 \
+# --indent --add-comments=TRANSLATORS: --no-location --sort-output \
+# --package-name=snappy \
+# --msgid-bugs-address=snappy-devel@lists.ubuntu.com \
+# --keyword=NG:1,2 --keyword=G \
+# $HERE/*/*.go $HERE/cmd/*/*.go
+
+# language packs
+for p in ${HERE}/po/*.po; do
+ lang=$(basename $p .po)
+ mkdir -p $HERE/share/locale/$lang/LC_MESSAGES
+ msgfmt -v -o $HERE/share/locale/$lang/LC_MESSAGES/snappy.mo $p
+done
--- /dev/null
+{
+ "comment": "",
+ "ignore": "test",
+ "package": [
+ {
+ "path": "context",
+ "revision": ""
+ },
+ {
+ "checksumSHA1": "f4UJKXymPS31lDRMfsmPA4PvwVw=",
+ "path": "github.com/cheggaaa/pb",
+ "revision": "6e9d17711bb763b26b68b3931d47f24c1323abab",
+ "revisionTime": "2016-08-12T10:57:48Z"
+ },
+ {
+ "checksumSHA1": "ab0MZfSfbTzqPQ1lVEJWpzZ5PJM=",
+ "path": "github.com/coreos/go-systemd/activation",
+ "revision": "f743bc15d6bddd23662280b4ad20f7c874cdd5ad",
+ "revisionTime": "2014-05-03T19:37:39Z"
+ },
+ {
+ "checksumSHA1": "iIUYZyoanCQQTUaWsu8b+iOSPt4=",
+ "path": "github.com/gorilla/context",
+ "revision": "1c83b3eabd45b6d76072b66b746c20815fb2872d",
+ "revisionTime": "2015-08-20T05:12:45Z"
+ },
+ {
+ "checksumSHA1": "8jXj6IMkk1B0LdIYgVG5Vn01/NU=",
+ "path": "github.com/gorilla/mux",
+ "revision": "ee1815431e497d3850809578c93ab6705f1a19f7",
+ "revisionTime": "2015-08-20T05:15:06Z"
+ },
+ {
+ "checksumSHA1": "2fapauoMahGWxKJdjdqpKfJpAOM=",
+ "path": "github.com/gorilla/websocket",
+ "revision": "234959944d9cf05229b02e8b386e5cffe1e4e04a",
+ "revisionTime": "2016-01-28T16:48:56Z"
+ },
+ {
+ "checksumSHA1": "Fsff4Yngdyqbq9ulSyTT4LGrxck=",
+ "path": "github.com/jessevdk/go-flags",
+ "revision": "6b9493b3cb60367edd942144879646604089e3f7",
+ "revisionTime": "2016-02-27T09:34:14Z"
+ },
+ {
+ "checksumSHA1": "MXOfmoFN5PBz/N5pDNrFe9ugYf8=",
+ "path": "github.com/mvo5/gettext.go",
+ "revision": "da4fdf605f1b0e2aa523423db5c0a3f727d62019",
+ "revisionTime": "2016-12-13T07:12:38Z"
+ },
+ {
+ "checksumSHA1": "wClBtrHsGvzusCkIFdF78Iak6y0=",
+ "path": "github.com/mvo5/gettext.go/pluralforms",
+ "revision": "da4fdf605f1b0e2aa523423db5c0a3f727d62019",
+ "revisionTime": "2016-12-13T07:12:38Z"
+ },
+ {
+ "checksumSHA1": "bzUdFxQ29mPK0lwgFVcF0GFN74Q=",
+ "path": "github.com/mvo5/goconfigparser",
+ "revision": "26426272dda20cc76aa1fa44286dc743d2972fe8",
+ "revisionTime": "2015-02-12T09:37:50Z"
+ },
+ {
+ "checksumSHA1": "VV5OVESr0GIy7mRuCjpve1U4/zM=",
+ "path": "github.com/mvo5/uboot-go",
+ "revision": "361f6ebcbb54f389d15dc9faefa000e996ba3e37",
+ "revisionTime": "2015-07-22T06:53:40Z"
+ },
+ {
+ "checksumSHA1": "33255TmqBPhD+hcO4kzuzyJc23o=",
+ "path": "github.com/mvo5/uboot-go/uenv",
+ "revision": "361f6ebcbb54f389d15dc9faefa000e996ba3e37",
+ "revisionTime": "2015-07-22T06:53:40Z"
+ },
+ {
+ "checksumSHA1": "G1Zy6KNKWSz6Nx6GAgaKM2yPDLg=",
+ "path": "github.com/testing-cabal/subunit-go",
+ "revision": "00b258565a5cf3adaa24b68d31c9e6ec3d2cdbe7",
+ "revisionTime": "2015-11-09T18:16:47Z"
+ },
+ {
+ "checksumSHA1": "E28iXtNf9DZVsymruqAnTFGNNnE=",
+ "path": "golang.org/x/crypto",
+ "revision": "351dc6a5bf92a5f2ae22fadeee08eb6a45aa2d93",
+ "revisionTime": "2016-08-24T13:50:57Z",
+ "tree": true
+ },
+ {
+ "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
+ "path": "golang.org/x/net/context",
+ "revision": "6250b412798208e6c90b03b7c4f226de5aa299e2",
+ "revisionTime": "2016-08-24T22:20:41Z"
+ },
+ {
+ "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=",
+ "path": "golang.org/x/net/context/ctxhttp",
+ "revision": "6250b412798208e6c90b03b7c4f226de5aa299e2",
+ "revisionTime": "2016-08-24T22:20:41Z"
+ },
+ {
+ "checksumSHA1": "XeOje6FklwIZfFMTgE583al/ZDo=",
+ "path": "gopkg.in/check.v1",
+ "revision": "64131543e7896d5bcc6bd5a76287eb75ea96c673",
+ "revisionTime": "2014-10-24T13:38:53Z"
+ },
+ {
+ "checksumSHA1": "mWrTCVjXPLevlqKUqpzSoEM3tu8=",
+ "path": "gopkg.in/macaroon.v1",
+ "revision": "ab3940c6c16510a850e1c2dd628b919f0f3f1464",
+ "revisionTime": "2015-01-21T11:42:31Z"
+ },
+ {
+ "checksumSHA1": "lBMMakT63h9ywP4d5wlkhOYMCAs=",
+ "path": "gopkg.in/retry.v1",
+ "revision": "c09f6b86ba4d5d2cf5bdf0665364aec9fd4815db",
+ "revisionTime": "2016-10-25T18:07:18Z"
+ },
+ {
+ "checksumSHA1": "t95/FNK2nGCx3e6IARbO7OrcCuY=",
+ "path": "gopkg.in/tomb.v2",
+ "revision": "d5d1b5820637886def9eef33e03a27a9f166942c",
+ "revisionTime": "2016-12-08T15:16:19Z"
+ },
+ {
+ "checksumSHA1": "hiOUviIMDKo7y918uBiEhfJyOkk=",
+ "path": "gopkg.in/tylerb/graceful.v1",
+ "revision": "50a48b6e73fcc75b45e22c05b79629a67c79e938",
+ "revisionTime": "2016-08-29T01:00:30Z"
+ },
+ {
+ "checksumSHA1": "TXfll3dCoaPKSntwHO0yR8eZ4JY=",
+ "path": "gopkg.in/yaml.v2",
+ "revision": "49c95bdc21843256fb6c4e0d370a05f24a0bf213",
+ "revisionTime": "2015-02-24T22:57:58Z"
+ }
+ ],
+ "rootPath": "github.com/snapcore/snapd"
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package wrappers is used to generate wrappers and service units and also desktop files for snap applications.
+package wrappers
+
+import (
+ "os"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/snap"
+)
+
+// AddSnapBinaries writes the wrapper binaries for the applications from the snap which aren't services.
+func AddSnapBinaries(s *snap.Info) error {
+ if err := os.MkdirAll(dirs.SnapBinariesDir, 0755); err != nil {
+ return err
+ }
+
+ for _, app := range s.Apps {
+ if app.Daemon != "" {
+ continue
+ }
+
+ if err := os.Remove(app.WrapperPath()); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ if err := os.Symlink("/usr/bin/snap", app.WrapperPath()); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// RemoveSnapBinaries removes the wrapper binaries for the applications from the snap which aren't services from.
+func RemoveSnapBinaries(s *snap.Info) error {
+ for _, app := range s.Apps {
+ os.Remove(app.WrapperPath())
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers_test
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/wrappers"
+)
+
+func TestWrappers(t *testing.T) { TestingT(t) }
+
+type binariesTestSuite struct {
+ tempdir string
+}
+
+var _ = Suite(&binariesTestSuite{})
+
+func (s *binariesTestSuite) SetUpTest(c *C) {
+ s.tempdir = c.MkDir()
+ dirs.SetRootDir(s.tempdir)
+}
+
+func (s *binariesTestSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+const packageHello = `name: hello-snap
+version: 1.10
+summary: hello
+description: Hello...
+apps:
+ hello:
+ command: bin/hello
+ svc1:
+ command: bin/hello
+ stop-command: bin/goodbye
+ post-stop-command: bin/missya
+ daemon: forking
+`
+const contentsHello = "HELLO"
+
+func (s *binariesTestSuite) TestAddSnapBinariesAndRemove(c *C) {
+ info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(11)})
+
+ err := wrappers.AddSnapBinaries(info)
+ c.Assert(err, IsNil)
+
+ link := filepath.Join(s.tempdir, "/snap/bin/hello-snap.hello")
+ target, err := os.Readlink(link)
+ c.Assert(err, IsNil)
+ c.Check(target, Equals, "/usr/bin/snap")
+
+ err = wrappers.RemoveSnapBinaries(info)
+ c.Assert(err, IsNil)
+
+ c.Check(osutil.FileExists(link), Equals, false)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+// valid simple prefixes
+var validDesktopFilePrefixes = []string{
+ // headers
+ "[Desktop Entry]",
+ "[Desktop Action ",
+ // https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html
+ "Type=",
+ "Version=",
+ "Name=",
+ "GenericName=",
+ "NoDisplay=",
+ "Comment=",
+ "Icon=",
+ "Hidden=",
+ "OnlyShowIn=",
+ "NotShowIn=",
+ "Exec=",
+ // Note that we do not support TryExec, it does not make sense
+ // in the snap context
+ "Terminal=",
+ "Actions=",
+ "MimeType=",
+ "Categories=",
+ "Keywords=",
+ "StartupNotify=",
+ "StartupWMClass=",
+}
+
+// name desktop file keys are localized as key[LOCALE]=:
+// lang_COUNTRY@MODIFIER
+// lang_COUNTRY
+// lang@MODIFIER
+// lang
+var validLocalizedDesktopFilePrefixes = []string{
+ "Name",
+ "GenericName",
+ "Comment",
+ "Keywords",
+}
+
+func isValidDesktopFilePrefix(line string) bool {
+ for _, prefix := range validDesktopFilePrefixes {
+ if strings.HasPrefix(line, prefix) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func trimLang(s string) string {
+ const langChars = "@_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+ if s == "" || s[0] != '[' {
+ return s
+ }
+ t := strings.TrimLeft(s[1:], langChars)
+ if t != "" && t[0] == ']' {
+ return t[1:]
+ }
+ return s
+}
+
+func isValidLocalizedDesktopFilePrefix(line string) bool {
+ for _, prefix := range validLocalizedDesktopFilePrefixes {
+ s := strings.TrimPrefix(line, prefix)
+ if s == line {
+ continue
+ }
+ if strings.HasPrefix(trimLang(s), "=") {
+ return true
+ }
+ }
+ return false
+}
+
+// rewriteExecLine rewrites a "Exec=" line to use the wrapper path for snap application.
+func rewriteExecLine(s *snap.Info, desktopFile, line string) (string, error) {
+ cmd := strings.SplitN(line, "=", 2)[1]
+ for _, app := range s.Apps {
+ env := fmt.Sprintf("env BAMF_DESKTOP_FILE_HINT=%s ", desktopFile)
+ wrapper := app.WrapperPath()
+ validCmd := filepath.Base(wrapper)
+ // check the prefix to allow %flag style args
+ // this is ok because desktop files are not run through sh
+ // so we don't have to worry about the arguments too much
+ if cmd == validCmd {
+ return "Exec=" + env + wrapper, nil
+ } else if strings.HasPrefix(cmd, validCmd+" ") {
+ return fmt.Sprintf("Exec=%s%s%s", env, wrapper, line[len("Exec=")+len(validCmd):]), nil
+ }
+ }
+
+ return "", fmt.Errorf("invalid exec command: %q", cmd)
+}
+
+func sanitizeDesktopFile(s *snap.Info, desktopFile string, rawcontent []byte) []byte {
+ newContent := []string{}
+
+ scanner := bufio.NewScanner(bytes.NewReader(rawcontent))
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ // whitespace/comments are just copied
+ if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
+ newContent = append(newContent, line)
+ continue
+ }
+
+ // ignore everything we have not whitelisted
+ if !isValidDesktopFilePrefix(line) && !isValidLocalizedDesktopFilePrefix(line) {
+ continue
+ }
+ // rewrite exec lines to an absolute path for the binary
+ if strings.HasPrefix(line, "Exec=") {
+ var err error
+ line, err = rewriteExecLine(s, desktopFile, line)
+ if err != nil {
+ // something went wrong, ignore the line
+ continue
+ }
+ }
+
+ // do variable substitution
+ line = strings.Replace(line, "${SNAP}", s.MountDir(), -1)
+ newContent = append(newContent, line)
+ }
+
+ return []byte(strings.Join(newContent, "\n"))
+}
+
+func updateDesktopDatabase(desktopFiles []string) error {
+ if len(desktopFiles) == 0 {
+ return nil
+ }
+
+ if _, err := exec.LookPath("update-desktop-database"); err == nil {
+ if output, err := exec.Command("update-desktop-database", dirs.SnapDesktopFilesDir).CombinedOutput(); err != nil {
+ return fmt.Errorf("cannot update-desktop-database %q: %s", output, err)
+ }
+ logger.Debugf("update-desktop-database successful")
+ }
+ return nil
+}
+
+// AddSnapDesktopFiles puts in place the desktop files for the applications from the snap.
+func AddSnapDesktopFiles(s *snap.Info) error {
+ if err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755); err != nil {
+ return err
+ }
+
+ baseDir := s.MountDir()
+
+ desktopFiles, err := filepath.Glob(filepath.Join(baseDir, "meta", "gui", "*.desktop"))
+ if err != nil {
+ return fmt.Errorf("cannot get desktop files for %v: %s", baseDir, err)
+ }
+
+ for _, df := range desktopFiles {
+ content, err := ioutil.ReadFile(df)
+ if err != nil {
+ return err
+ }
+
+ installedDesktopFileName := filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s", s.Name(), filepath.Base(df)))
+ content = sanitizeDesktopFile(s, installedDesktopFileName, content)
+ if err := osutil.AtomicWriteFile(installedDesktopFileName, []byte(content), 0755, 0); err != nil {
+ return err
+ }
+ }
+
+ // updates mime info etc
+ if err := updateDesktopDatabase(desktopFiles); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// RemoveSnapDesktopFiles removes the added desktop files for the applications in the snap.
+func RemoveSnapDesktopFiles(s *snap.Info) error {
+ glob := filepath.Join(dirs.SnapDesktopFilesDir, s.Name()+"_*.desktop")
+ activeDesktopFiles, err := filepath.Glob(glob)
+ if err != nil {
+ return fmt.Errorf("cannot get desktop files for %v: %s", glob, err)
+ }
+ for _, f := range activeDesktopFiles {
+ os.Remove(f)
+ }
+
+ // updates mime info etc
+ if err := updateDesktopDatabase(activeDesktopFiles); err != nil {
+ return err
+ }
+
+ return nil
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+ "github.com/snapcore/snapd/wrappers"
+)
+
+type desktopSuite struct {
+ tempdir string
+
+ mockUpdateDesktopDatabase *testutil.MockCmd
+}
+
+var _ = Suite(&desktopSuite{})
+
+func (s *desktopSuite) SetUpTest(c *C) {
+ s.tempdir = c.MkDir()
+ dirs.SetRootDir(s.tempdir)
+
+ s.mockUpdateDesktopDatabase = testutil.MockCommand(c, "update-desktop-database", "")
+}
+
+func (s *desktopSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+var desktopAppYaml = `
+name: foo
+version: 1.0
+`
+
+var mockDesktopFile = []byte(`
+[Desktop Entry]
+Name=foo
+Icon=${SNAP}/foo.png`)
+var desktopContents = ""
+
+func (s *desktopSuite) TestAddPackageDesktopFiles(c *C) {
+ expectedDesktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "foo_foobar.desktop")
+ c.Assert(osutil.FileExists(expectedDesktopFilePath), Equals, false)
+
+ info := snaptest.MockSnap(c, desktopAppYaml, desktopContents, &snap.SideInfo{Revision: snap.R(11)})
+
+ // generate .desktop file in the package baseDir
+ baseDir := info.MountDir()
+ err := os.MkdirAll(filepath.Join(baseDir, "meta", "gui"), 0755)
+ c.Assert(err, IsNil)
+
+ err = ioutil.WriteFile(filepath.Join(baseDir, "meta", "gui", "foobar.desktop"), mockDesktopFile, 0644)
+ c.Assert(err, IsNil)
+
+ err = wrappers.AddSnapDesktopFiles(info)
+ c.Assert(err, IsNil)
+ c.Assert(osutil.FileExists(expectedDesktopFilePath), Equals, true)
+ c.Assert(s.mockUpdateDesktopDatabase.Calls(), DeepEquals, [][]string{
+ {"update-desktop-database", dirs.SnapDesktopFilesDir},
+ })
+}
+
+func (s *desktopSuite) TestRemovePackageDesktopFiles(c *C) {
+ mockDesktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "foo_foobar.desktop")
+
+ err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(mockDesktopFilePath, mockDesktopFile, 0644)
+ c.Assert(err, IsNil)
+ info, err := snap.InfoFromSnapYaml([]byte(desktopAppYaml))
+ c.Assert(err, IsNil)
+
+ err = wrappers.RemoveSnapDesktopFiles(info)
+ c.Assert(err, IsNil)
+ c.Assert(osutil.FileExists(mockDesktopFilePath), Equals, false)
+ c.Assert(s.mockUpdateDesktopDatabase.Calls(), DeepEquals, [][]string{
+ {"update-desktop-database", dirs.SnapDesktopFilesDir},
+ })
+}
+
+// sanitize
+
+type sanitizeDesktopFileSuite struct{}
+
+var _ = Suite(&sanitizeDesktopFileSuite{})
+
+func (s *sanitizeDesktopFileSuite) TestSanitizeIgnoreNotWhitelisted(c *C) {
+ snap := &snap.Info{SideInfo: snap.SideInfo{RealName: "foo", Revision: snap.R(12)}}
+ desktopContent := []byte(`[Desktop Entry]
+Name=foo
+UnknownKey=baz
+nonsense
+Icon=${SNAP}/meep
+
+# the empty line above is fine`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, fmt.Sprintf(`[Desktop Entry]
+Name=foo
+Icon=%s/foo/12/meep
+
+# the empty line above is fine`, dirs.SnapMountDir))
+}
+
+func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExec(c *C) {
+ snap, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+version: 1.0
+apps:
+ app:
+ command: cmd
+`))
+ c.Assert(err, IsNil)
+ desktopContent := []byte(`[Desktop Entry]
+Name=foo
+Exec=baz
+`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, `[Desktop Entry]
+Name=foo`)
+}
+
+func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExecPrefix(c *C) {
+ snap, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+version: 1.0
+apps:
+ app:
+ command: cmd
+`))
+ c.Assert(err, IsNil)
+ desktopContent := []byte(`[Desktop Entry]
+Name=foo
+Exec=snap.app.evil.evil
+`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, `[Desktop Entry]
+Name=foo`)
+}
+
+func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExecOk(c *C) {
+ snap, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+version: 1.0
+apps:
+ app:
+ command: cmd
+`))
+ c.Assert(err, IsNil)
+ desktopContent := []byte(`[Desktop Entry]
+Name=foo
+Exec=snap.app %U
+`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, fmt.Sprintf(`[Desktop Entry]
+Name=foo
+Exec=env BAMF_DESKTOP_FILE_HINT=foo.desktop %s/bin/snap.app %%U`, dirs.SnapMountDir))
+}
+
+// we do not support TryExec (even if its a valid line), this test ensures
+// we do not accidentally enable it
+func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersTryExecIgnored(c *C) {
+ snap, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+version: 1.0
+apps:
+ app:
+ command: cmd
+`))
+ c.Assert(err, IsNil)
+ desktopContent := []byte(`[Desktop Entry]
+Name=foo
+TryExec=snap.app %U
+`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, `[Desktop Entry]
+Name=foo`)
+}
+
+func (s *sanitizeDesktopFileSuite) TestSanitizeWorthWithI18n(c *C) {
+ snap := &snap.Info{}
+ desktopContent := []byte(`[Desktop Entry]
+Name=foo
+GenericName=bar
+GenericName[de]=einsehrlangeszusammengesetzteswort
+GenericName[tlh_TLH]=Qapla'
+GenericName[ca@valencia]=Hola!
+Invalid=key
+Invalid[i18n]=key
+`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, `[Desktop Entry]
+Name=foo
+GenericName=bar
+GenericName[de]=einsehrlangeszusammengesetzteswort
+GenericName[tlh_TLH]=Qapla'
+GenericName[ca@valencia]=Hola!`)
+}
+
+func (s *sanitizeDesktopFileSuite) TestSanitizeDesktopActionsOk(c *C) {
+ snap := &snap.Info{}
+ desktopContent := []byte(`[Desktop Action is-ok]`)
+
+ e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent)
+ c.Assert(string(e), Equals, `[Desktop Action is-ok]`)
+}
+
+func (s *sanitizeDesktopFileSuite) TestRewriteExecLineInvalid(c *C) {
+ snap := &snap.Info{}
+ _, err := wrappers.RewriteExecLine(snap, "foo.desktop", "Exec=invalid")
+ c.Assert(err, ErrorMatches, `invalid exec command: "invalid"`)
+}
+
+func (s *sanitizeDesktopFileSuite) TestRewriteExecLineOk(c *C) {
+ snap, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+version: 1.0
+apps:
+ app:
+ command: cmd
+`))
+ c.Assert(err, IsNil)
+
+ newl, err := wrappers.RewriteExecLine(snap, "foo.desktop", "Exec=snap.app")
+ c.Assert(err, IsNil)
+ c.Assert(newl, Equals, fmt.Sprintf("Exec=env BAMF_DESKTOP_FILE_HINT=foo.desktop %s/bin/snap.app", dirs.SnapMountDir))
+}
+
+func (s *sanitizeDesktopFileSuite) TestTrimLang(c *C) {
+ langs := []struct {
+ in string
+ out string
+ }{
+ // langCodes
+ {"[lang_COUNTRY@MODIFIER]=foo", "=foo"},
+ {"[lang_COUNTRY]=bar", "=bar"},
+ {"[lang_COUNTRY]=baz", "=baz"},
+ {"[lang]=foobar", "=foobar"},
+ // non-langCodes, should be ignored
+ {"", ""},
+ {"Name=foobar", "Name=foobar"},
+ // corner case
+ {"[foo=bar", "[foo=bar"},
+ }
+ for _, t := range langs {
+ c.Assert(wrappers.TrimLang(t.in), Equals, t.out)
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers
+
+import (
+ "time"
+)
+
+// some internal helper exposed for testing
+var (
+ // services
+ GenerateSnapServiceFile = generateSnapServiceFile
+
+ // desktop
+ SanitizeDesktopFile = sanitizeDesktopFile
+ RewriteExecLine = rewriteExecLine
+ TrimLang = trimLang
+)
+
+func MockKillWait(wait time.Duration) (restore func()) {
+ oldKillWait := killWait
+ killWait = wait
+ return func() {
+ killWait = oldKillWait
+ }
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "text/template"
+ "time"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/timeout"
+)
+
+type interacter interface {
+ Notify(status string)
+}
+
+// wait this time between TERM and KILL
+var killWait = 5 * time.Second
+
+func serviceStopTimeout(app *snap.AppInfo) time.Duration {
+ tout := app.StopTimeout
+ if tout == 0 {
+ tout = timeout.DefaultTimeout
+ }
+ return time.Duration(tout)
+}
+
+func generateSnapServiceFile(app *snap.AppInfo) (string, error) {
+ if err := snap.ValidateApp(app); err != nil {
+ return "", err
+ }
+
+ return genServiceFile(app), nil
+}
+
+// StartSnapServices starts service units for the applications from the snap which are services.
+func StartSnapServices(s *snap.Info, inter interacter) error {
+ for _, app := range s.Apps {
+ if app.Daemon == "" {
+ continue
+ }
+ // daemon-reload and enable plus start
+ serviceName := filepath.Base(app.ServiceFile())
+ sysd := systemd.New(dirs.GlobalRootDir, inter)
+ if err := sysd.DaemonReload(); err != nil {
+ return err
+ }
+
+ if err := sysd.Enable(serviceName); err != nil {
+ return err
+ }
+
+ if err := sysd.Start(serviceName); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// AddSnapServices adds service units for the applications from the snap which are services.
+func AddSnapServices(s *snap.Info, inter interacter) error {
+ for _, app := range s.Apps {
+ if app.Daemon == "" {
+ continue
+ }
+ // Generate service file
+ content, err := generateSnapServiceFile(app)
+ if err != nil {
+ return err
+ }
+ svcFilePath := app.ServiceFile()
+ os.MkdirAll(filepath.Dir(svcFilePath), 0755)
+ if err := osutil.AtomicWriteFile(svcFilePath, []byte(content), 0644, 0); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// StopSnapServices stops service units for the applications from the snap which are services.
+func StopSnapServices(s *snap.Info, inter interacter) error {
+ sysd := systemd.New(dirs.GlobalRootDir, inter)
+
+ for _, app := range s.Apps {
+ // Handle the case where service file doesn't exist and don't try to stop it as it will fail.
+ // This can happen with snap try when snap.yaml is modified on the fly and a daemon line is added.
+ if app.Daemon == "" || !osutil.FileExists(app.ServiceFile()) {
+ continue
+ }
+ serviceName := filepath.Base(app.ServiceFile())
+ tout := serviceStopTimeout(app)
+ if err := sysd.Stop(serviceName, tout); err != nil {
+ if !systemd.IsTimeout(err) {
+ return err
+ }
+ inter.Notify(fmt.Sprintf("%s refused to stop, killing.", serviceName))
+ // ignore errors for kill; nothing we'd do differently at this point
+ sysd.Kill(serviceName, "TERM")
+ time.Sleep(killWait)
+ sysd.Kill(serviceName, "KILL")
+ }
+ }
+
+ return nil
+
+}
+
+// RemoveSnapServices disables and removes service units for the applications from the snap which are services.
+func RemoveSnapServices(s *snap.Info, inter interacter) error {
+ sysd := systemd.New(dirs.GlobalRootDir, inter)
+
+ nservices := 0
+
+ for _, app := range s.Apps {
+ if app.Daemon == "" || !osutil.FileExists(app.ServiceFile()) {
+ continue
+ }
+ nservices++
+
+ serviceName := filepath.Base(app.ServiceFile())
+ if err := sysd.Disable(serviceName); err != nil {
+ return err
+ }
+
+ if err := os.Remove(app.ServiceFile()); err != nil && !os.IsNotExist(err) {
+ logger.Noticef("Failed to remove service file for %q: %v", serviceName, err)
+ }
+
+ if err := os.Remove(app.ServiceSocketFile()); err != nil && !os.IsNotExist(err) {
+ logger.Noticef("Failed to remove socket file for %q: %v", serviceName, err)
+ }
+ }
+
+ // only reload if we actually had services
+ if nservices > 0 {
+ if err := sysd.DaemonReload(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func genServiceFile(appInfo *snap.AppInfo) string {
+ serviceTemplate := `[Unit]
+# Auto-generated, DO NO EDIT
+Description=Service for snap application {{.App.Snap.Name}}.{{.App.Name}}
+Requires={{.MountUnit}}
+Wants={{.PrerequisiteTarget}}
+After={{.MountUnit}} {{.PrerequisiteTarget}}
+X-Snappy=yes
+
+[Service]
+ExecStart={{.App.LauncherCommand}}
+Restart={{.Restart}}
+WorkingDirectory={{.App.Snap.DataDir}}
+{{if .App.StopCommand}}ExecStop={{.App.LauncherStopCommand}}{{end}}
+{{if .App.PostStopCommand}}ExecStopPost={{.App.LauncherPostStopCommand}}{{end}}
+{{if .StopTimeout}}TimeoutStopSec={{.StopTimeout.Seconds}}{{end}}
+Type={{.App.Daemon}}
+{{if .App.BusName}}BusName={{.App.BusName}}{{end}}
+
+[Install]
+WantedBy={{.ServicesTarget}}
+`
+ var templateOut bytes.Buffer
+ t := template.Must(template.New("service-wrapper").Parse(serviceTemplate))
+
+ var restartCond string
+ if appInfo.RestartCond == systemd.RestartNever {
+ restartCond = "no"
+ } else {
+ restartCond = appInfo.RestartCond.String()
+ }
+ if restartCond == "" {
+ restartCond = systemd.RestartOnFailure.String()
+ }
+
+ wrapperData := struct {
+ App *snap.AppInfo
+
+ Restart string
+ StopTimeout time.Duration
+ ServicesTarget string
+ PrerequisiteTarget string
+ MountUnit string
+
+ Home string
+ EnvVars string
+ }{
+ App: appInfo,
+
+ Restart: restartCond,
+ StopTimeout: serviceStopTimeout(appInfo),
+ ServicesTarget: systemd.ServicesTarget,
+ PrerequisiteTarget: systemd.PrerequisiteTarget,
+ MountUnit: filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())),
+
+ // systemd runs as PID 1 so %h will not work.
+ Home: "/root",
+ }
+
+ if err := t.Execute(&templateOut, wrapperData); err != nil {
+ // this can never happen, except we forget a variable
+ logger.Panicf("Unable to execute template: %v", err)
+ }
+
+ return templateOut.String()
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers_test
+
+import (
+ "fmt"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/timeout"
+ "github.com/snapcore/snapd/wrappers"
+)
+
+type servicesWrapperGenSuite struct{}
+
+var _ = Suite(&servicesWrapperGenSuite{})
+
+const expectedServiceFmt = `[Unit]
+# Auto-generated, DO NO EDIT
+Description=Service for snap application snap.app
+Requires=snap-snap-44.mount
+Wants=network-online.target
+After=snap-snap-44.mount network-online.target
+X-Snappy=yes
+
+[Service]
+ExecStart=/usr/bin/snap run snap.app
+Restart=on-failure
+WorkingDirectory=/var/snap/snap/44
+ExecStop=/usr/bin/snap run --command=stop snap.app
+ExecStopPost=/usr/bin/snap run --command=post-stop snap.app
+TimeoutStopSec=10
+Type=%s
+
+[Install]
+WantedBy=multi-user.target
+`
+
+var (
+ expectedAppService = fmt.Sprintf(expectedServiceFmt, "simple\n")
+ expectedDbusService = fmt.Sprintf(expectedServiceFmt, "dbus\nBusName=foo.bar.baz")
+)
+
+var (
+ expectedServiceWrapperFmt = `[Unit]
+# Auto-generated, DO NO EDIT
+Description=Service for snap application xkcd-webserver.xkcd-webserver
+Requires=snap-xkcd\x2dwebserver-44.mount
+Wants=network-online.target
+After=snap-xkcd\x2dwebserver-44.mount network-online.target
+X-Snappy=yes
+
+[Service]
+ExecStart=/usr/bin/snap run xkcd-webserver
+Restart=on-failure
+WorkingDirectory=/var/snap/xkcd-webserver/44
+ExecStop=/usr/bin/snap run --command=stop xkcd-webserver
+ExecStopPost=/usr/bin/snap run --command=post-stop xkcd-webserver
+TimeoutStopSec=30
+Type=%s
+%s
+`
+ expectedTypeForkingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, "forking\n", "\n[Install]\nWantedBy=multi-user.target")
+)
+
+func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFile(c *C) {
+ yamlText := `
+name: snap
+version: 1.0
+apps:
+ app:
+ command: bin/start
+ stop-command: bin/stop
+ post-stop-command: bin/stop --post
+ stop-timeout: 10s
+ daemon: simple
+`
+ info, err := snap.InfoFromSnapYaml([]byte(yamlText))
+ c.Assert(err, IsNil)
+ info.Revision = snap.R(44)
+ app := info.Apps["app"]
+
+ generatedWrapper, err := wrappers.GenerateSnapServiceFile(app)
+ c.Assert(err, IsNil)
+ c.Check(generatedWrapper, Equals, expectedAppService)
+}
+
+func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileRestart(c *C) {
+ yamlTextTemplate := `
+name: snap
+apps:
+ app:
+ restart-condition: %s
+`
+ for name, cond := range systemd.RestartMap {
+ yamlText := fmt.Sprintf(yamlTextTemplate, cond)
+
+ info, err := snap.InfoFromSnapYaml([]byte(yamlText))
+ c.Assert(err, IsNil)
+ info.Revision = snap.R(44)
+ app := info.Apps["app"]
+
+ wrapperText, err := wrappers.GenerateSnapServiceFile(app)
+ c.Assert(err, IsNil)
+ if cond == systemd.RestartNever {
+ c.Check(wrapperText, Matches,
+ `(?ms).*^Restart=no$.*`, Commentf(name))
+ } else {
+ c.Check(wrapperText, Matches,
+ `(?ms).*^Restart=`+name+`$.*`, Commentf(name))
+ }
+ }
+}
+
+func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileTypeForking(c *C) {
+ service := &snap.AppInfo{
+ Snap: &snap.Info{
+ SuggestedName: "xkcd-webserver",
+ Version: "0.3.4",
+ SideInfo: snap.SideInfo{Revision: snap.R(44)},
+ },
+ Name: "xkcd-webserver",
+ Command: "bin/foo start",
+ StopCommand: "bin/foo stop",
+ PostStopCommand: "bin/foo post-stop",
+ StopTimeout: timeout.DefaultTimeout,
+ Daemon: "forking",
+ }
+
+ generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
+ c.Assert(err, IsNil)
+ c.Assert(generatedWrapper, Equals, expectedTypeForkingWrapper)
+}
+
+func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileIllegalChars(c *C) {
+ service := &snap.AppInfo{
+ Snap: &snap.Info{
+ SuggestedName: "xkcd-webserver",
+ Version: "0.3.4",
+ SideInfo: snap.SideInfo{Revision: snap.R(44)},
+ },
+ Name: "xkcd-webserver",
+ Command: "bin/foo start\n",
+ StopCommand: "bin/foo stop",
+ PostStopCommand: "bin/foo post-stop",
+ StopTimeout: timeout.DefaultTimeout,
+ Daemon: "simple",
+ }
+
+ _, err := wrappers.GenerateSnapServiceFile(service)
+ c.Assert(err, NotNil)
+}
+
+func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusName(c *C) {
+
+ yamlText := `
+name: snap
+version: 1.0
+apps:
+ app:
+ command: bin/start
+ stop-command: bin/stop
+ post-stop-command: bin/stop --post
+ stop-timeout: 10s
+ bus-name: foo.bar.baz
+ daemon: dbus
+`
+
+ info, err := snap.InfoFromSnapYaml([]byte(yamlText))
+ c.Assert(err, IsNil)
+ info.Revision = snap.R(44)
+ app := info.Apps["app"]
+
+ wrapperText, err := wrappers.GenerateSnapServiceFile(app)
+ c.Assert(err, IsNil)
+
+ c.Assert(wrapperText, Equals, expectedDbusService)
+}
--- /dev/null
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wrappers_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "regexp"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/systemd"
+ "github.com/snapcore/snapd/wrappers"
+)
+
+type servicesTestSuite struct {
+ tempdir string
+ prevctlCmd func(...string) ([]byte, error)
+}
+
+var _ = Suite(&servicesTestSuite{})
+
+func (s *servicesTestSuite) SetUpTest(c *C) {
+ s.tempdir = c.MkDir()
+ dirs.SetRootDir(s.tempdir)
+
+ s.prevctlCmd = systemd.SystemctlCmd
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ return []byte("ActiveState=inactive\n"), nil
+ }
+}
+
+func (s *servicesTestSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+ systemd.SystemctlCmd = s.prevctlCmd
+}
+
+func (s *servicesTestSuite) TestAddSnapServicesAndRemove(c *C) {
+ var sysdLog [][]string
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ sysdLog = append(sysdLog, cmd)
+ return []byte("ActiveState=inactive\n"), nil
+ }
+
+ info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(12)})
+
+ err := wrappers.AddSnapServices(info, nil)
+ c.Assert(err, IsNil)
+
+ svcFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.service")
+ content, err := ioutil.ReadFile(svcFile)
+ c.Assert(err, IsNil)
+
+ verbs := []string{"Start", "Stop", "StopPost"}
+ cmds := []string{"", " --command=stop", " --command=post-stop"}
+ for i := range verbs {
+ expected := fmt.Sprintf("Exec%s=/usr/bin/snap run%s hello-snap.svc1", verbs[i], cmds[i])
+ c.Check(string(content), Matches, "(?ms).*^"+regexp.QuoteMeta(expected)) // check.v1 adds ^ and $ around the regexp provided
+ }
+
+ sysdLog = nil
+ err = wrappers.StopSnapServices(info, &progress.NullProgress{})
+ c.Assert(err, IsNil)
+ c.Assert(sysdLog, HasLen, 2)
+ c.Check(sysdLog, DeepEquals, [][]string{
+ {"stop", filepath.Base(svcFile)},
+ {"show", "--property=ActiveState", "snap.hello-snap.svc1.service"},
+ })
+
+ sysdLog = nil
+ err = wrappers.RemoveSnapServices(info, &progress.NullProgress{})
+ c.Assert(err, IsNil)
+ c.Check(osutil.FileExists(svcFile), Equals, false)
+ c.Assert(sysdLog, HasLen, 2)
+ c.Check(sysdLog[0], DeepEquals, []string{"--root", dirs.GlobalRootDir, "disable", filepath.Base(svcFile)})
+ c.Check(sysdLog[1], DeepEquals, []string{"daemon-reload"})
+}
+
+func (s *servicesTestSuite) TestRemoveSnapPackageFallbackToKill(c *C) {
+ restore := wrappers.MockKillWait(200 * time.Millisecond)
+ defer restore()
+
+ var sysdLog [][]string
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ // filter out the "systemctl show" that
+ // StopSnapServicesGenerates
+ if cmd[0] != "show" {
+ sysdLog = append(sysdLog, cmd)
+ }
+ return []byte("ActiveState=active\n"), nil
+ }
+
+ info := snaptest.MockSnap(c, `name: wat
+version: 42
+apps:
+ wat:
+ command: wat
+ stop-timeout: 250ms
+ daemon: forking
+`, "", &snap.SideInfo{Revision: snap.R(11)})
+
+ err := wrappers.AddSnapServices(info, nil)
+ c.Assert(err, IsNil)
+
+ sysdLog = nil
+
+ svcFName := "snap.wat.wat.service"
+
+ err = wrappers.StopSnapServices(info, &progress.NullProgress{})
+ c.Assert(err, IsNil)
+
+ c.Check(sysdLog, DeepEquals, [][]string{
+ {"stop", svcFName},
+ // check kill invocations
+ {"kill", svcFName, "-s", "TERM"},
+ {"kill", svcFName, "-s", "KILL"},
+ })
+}
+
+func (s *servicesTestSuite) TestStartSnapServices(c *C) {
+ var sysdLog [][]string
+ systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) {
+ sysdLog = append(sysdLog, cmd)
+ return []byte("ActiveState=inactive\n"), nil
+ }
+
+ info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(12)})
+ svcFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.service")
+
+ err := wrappers.StartSnapServices(info, nil)
+ c.Assert(err, IsNil)
+
+ c.Assert(sysdLog, HasLen, 3)
+ c.Check(sysdLog[0], DeepEquals, []string{"daemon-reload"})
+ c.Check(sysdLog[1], DeepEquals, []string{"--root", dirs.GlobalRootDir, "enable", filepath.Base(svcFile)})
+ c.Check(sysdLog[2], DeepEquals, []string{"start", filepath.Base(svcFile)})
+}